๐Ÿฐ [flowbit] #5. Project Timeline - ์ž‘์—… ํ๋ฆ„์„ ํ”„๋กœ์ ํŠธ ํ๋ฆ„์œผ๋กœ ํ™•์žฅํ–ˆ๋‹ค

bean8080๐Ÿซ›ยท2026๋…„ 4์›” 30์ผ

flowbit ๐Ÿฐโ˜˜๏ธ

๋ชฉ๋ก ๋ณด๊ธฐ
5/15

โ˜˜๏ธ 1. ์˜ค๋Š˜ ๋ชฉํ‘œ

์–ด์ œ๋Š”
Project ๋„๋ฉ”์ธ์„ ์ถ”๊ฐ€ํ•ด์„œ

์ž‘์—…๋“ค์„ ํ•˜๋‚˜์˜ ํ๋ฆ„ ์•ˆ์— ๋„ฃ์—ˆ๋‹ค.

์ด์ œ ๊ตฌ์กฐ๋Š” ๊ฐ–์ถฐ์กŒ๋‹ค.

๊ทผ๋ฐ ์—ฌ๊ธฐ์„œ ๋˜ ํ•˜๋‚˜ ๋ถ€์กฑํ•œ ๊ฒŒ ์žˆ์—ˆ๋‹ค.

โ†’ ํ”„๋กœ์ ํŠธ ๋‹จ์œ„๋กœ ํ๋ฆ„์ด ์•ˆ ๋ณด์ธ๋‹ค

์ง€๊ธˆ์€

Task ํ•˜๋‚˜์˜ ํ๋ฆ„์€ ๋ณผ ์ˆ˜ ์žˆ์ง€๋งŒ
Project ์ „์ฒด ํ๋ฆ„์€ ๋ณผ ์ˆ˜ ์—†๋‹ค

๊ทธ๋ž˜์„œ ์˜ค๋Š˜ ๋ชฉํ‘œ๋Š” ์ด๊ฑฐ์˜€๋‹ค.

โ†’ ํ”„๋กœ์ ํŠธ ์•ˆ์—์„œ ์–ด๋–ค ์ผ์ด ์žˆ์—ˆ๋Š”์ง€ ํ•œ๋ˆˆ์— ๋ณด์ด๊ฒŒ ๋งŒ๋“ค์ž

์˜ˆ๋ฅผ ๋“ค์–ด,

์—ฌ๋Ÿฌ ์ž‘์—…์ด ๋™์‹œ์— ์ง„ํ–‰๋˜๋Š” ์ƒํ™ฉ์—์„œ
๊ฐ ์ž‘์—…์„ ํ•˜๋‚˜์”ฉ ๋ณด๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ

์ด ํ”„๋กœ์ ํŠธ์—์„œ ์‹ค์ œ๋กœ ์–ด๋–ค ์ผ๋“ค์ด ์‹œ๊ฐ„ ์ˆœ์œผ๋กœ ์ผ์–ด๋‚ฌ๋Š”์ง€
ํ•œ ๋ฒˆ์— ๋ณด๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค.


โ˜˜๏ธ 2. Task ํƒ€์ž„๋ผ์ธ์˜ ํ•œ๊ณ„

์ด๋ฏธ ์ด๋Ÿฐ API๋Š” ์žˆ๋‹ค.

GET /tasks/{id}/timeline

์ด๊ฑด ์ž‘์—… ํ•˜๋‚˜์˜ ํ๋ฆ„์„ ๋ณด์—ฌ์ค€๋‹ค.

๊ทผ๋ฐ ์‹ค์ œ๋กœ ๊ถ๊ธˆํ•œ ๊ฑด ์ด๊ฑฐ๋‹ค.

โ†’ ์ด ํ”„๋กœ์ ํŠธ์—์„œ ์‹ค์ œ๋กœ ๋ฌด์Šจ ์ผ์ด ์žˆ์—ˆ๋Š”๊ฐ€?

์ž‘์—… ํ•˜๋‚˜๋งŒ ๋ณด๋ฉด ์—ฌ์ „ํžˆ ๋ถ€๋ถ„์ด๋‹ค.

๊ทธ๋ž˜์„œ ํ•„์š”ํ•œ ๊ฑด

โ†’ Project ๊ธฐ์ค€ ํ๋ฆ„

์ฆ‰,

์ž‘์—… ๋‹จ์œ„๊ฐ€ ์•„๋‹ˆ๋ผ
โ†’ ํ”„๋กœ์ ํŠธ ์ „์ฒด๊ฐ€ ์–ด๋–ป๊ฒŒ ํ˜๋Ÿฌ๊ฐ”๋Š”์ง€๋ฅผ ๋ด์•ผ ํ•œ๋‹ค.


โ˜˜๏ธ 3. Project Timeline API

๊ทธ๋ž˜์„œ ๋งŒ๋“  API๊ฐ€ ์ด๊ฑฐ๋‹ค.

GET /projects/{id}/timeline

์ด API๋Š”

ํ”„๋กœ์ ํŠธ์— ์†ํ•œ ๋ชจ๋“  Task๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ 
๊ทธ Task๋“ค์˜ ์ด๋ฒคํŠธ๋ฅผ ์ „๋ถ€ ๋ชจ์•„์„œ
์‹œ๊ฐ„์ˆœ์œผ๋กœ ์ •๋ ฌํ•ด์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค

Project Timeline ์กฐํšŒ ๊ฒฐ๊ณผ

ํ”„๋กœ์ ํŠธ ์•ˆ์—์„œ ๋ฐœ์ƒํ•œ ์—ฌ๋Ÿฌ ์ž‘์—… ์ด๋ฒคํŠธ๋“ค์ด
ํ•˜๋‚˜์˜ ์‹œ๊ฐ„ ํ๋ฆ„์œผ๋กœ ์ •๋ ฌ๋˜์–ด ๋‚ด๋ ค์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์ œ๋Š”

โ†’ Task ํ•˜๋‚˜๊ฐ€ ์•„๋‹ˆ๋ผ
โ†’ ํ”„๋กœ์ ํŠธ ์ „์ฒด ํ๋ฆ„์ด ๋ณด์ธ๋‹ค


โ˜˜๏ธ 4. ํ๋ฆ„์„ ํ•ฉ์น˜๋Š” ๋ฐฉ์‹

๊ตฌ์กฐ๋Š” ์ด๋ ‡๊ฒŒ ํ˜๋Ÿฌ๊ฐ„๋‹ค.

Project โ†’ Tasks โ†’ TaskEvents

์ฝ”๋“œ ํ๋ฆ„์œผ๋กœ ๋ณด๋ฉด ์ด๋ ‡๋‹ค.

List<Task> tasks = taskRepository.findByProject_IdAndStatusNot(projectId, TaskStatus.DELETED);

List<Long> taskIds = tasks.stream()
        .map(Task::getId)
        .toList();

List<TaskEvent> events = taskEventRepository.findByTaskIdInOrderByCreatedAtAsc(taskIds);

ํ”„๋กœ์ ํŠธ์— ์†ํ•œ Task๋ฅผ ๋จผ์ € ์กฐํšŒํ•œ ๋’ค,
ํ•ด๋‹น Task๋“ค์˜ id๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๋Š” ๊ตฌ์กฐ๋‹ค.

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ ์ด๊ฑฐ๋‹ค.

โ†’ ์—ฌ๋Ÿฌ Task์˜ ์ด๋ฒคํŠธ๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์นœ๋‹ค

์ฆ‰, Project ๊ธฐ์ค€์œผ๋กœ ์ง์ ‘ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ
Task๋ฅผ ๊ฑฐ์ณ Event๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ตฌ์กฐ๋‹ค.


โ˜˜๏ธ 5. ์™œ ์ด๋ ‡๊ฒŒ ํ–ˆ๋‚˜

TaskEvent๋Š” Task ๊ธฐ์ค€์œผ๋กœ ๊ธฐ๋ก๋œ๋‹ค.

๊ทธ๋ž˜์„œ ๋ฐ”๋กœ Project๋กœ ์กฐํšŒํ•  ์ˆ˜ ์—†๋‹ค.

์ค‘๊ฐ„ ๊ณผ์ •์ด ํ•„์š”ํ•˜๋‹ค.

Project๋กœ Task๋ฅผ ์ฐพ๊ณ 
Task๋กœ Event๋ฅผ ์ฐพ๋Š”๋‹ค

์ฆ‰

โ†’ ๊ด€๊ณ„๋ฅผ ํƒ€๊ณ  ์˜ฌ๋ผ๊ฐ€๋Š” ๊ตฌ์กฐ


โ˜˜๏ธ 6. ์‹œ๊ฐ„์ˆœ ์ •๋ ฌ์ด ํ•ต์‹ฌ โญ

์ด ๊ธฐ๋Šฅ์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด

โ†’ ์ •๋ ฌ

findByTaskIdInOrderByCreatedAtAsc(...)

์ด ํ•œ ์ค„๋กœ

์—ฌ๋Ÿฌ Task์˜ ์ด๋ฒคํŠธ๊ฐ€
ํ•˜๋‚˜์˜ ์‹œ๊ฐ„ ํ๋ฆ„์œผ๋กœ ์ •๋ฆฌ๋œ๋‹ค

๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ์œผ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ
ํฉ์–ด์ ธ ์žˆ๋˜ ์ด๋ฒคํŠธ๋“ค์„ ํ•˜๋‚˜์˜ ์‹œ๊ฐ„ ํ๋ฆ„์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ•ต์‹ฌ ํฌ์ธํŠธ๋‹ค.

์ด๊ฒŒ ์ค‘์š”ํ•œ ์ด์œ ๋Š”,

โ†’ ์—ฌ๋Ÿฌ ์ž‘์—…์ด ๋™์‹œ์— ์ง„ํ–‰๋˜๋Š” ์ƒํ™ฉ์—์„œ๋„
โ†’ ๋ฌด์Šจ ์ผ์ด ๋จผ์ € ์ผ์–ด๋‚ฌ๊ณ , ๊ทธ ๋‹ค์Œ์— ์–ด๋–ค ๋ณ€ํ™”๊ฐ€ ์žˆ์—ˆ๋Š”์ง€๋ฅผ
ํ•˜๋‚˜์˜ ํ๋ฆ„์œผ๋กœ ๋ณผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.


โ˜˜๏ธ 7. ๊ด€๊ณ„ ๋งคํ•‘ ๋ฆฌํŒฉํ† ๋ง (@ManyToOne)

์—ฌ๊ธฐ์„œ ํ•œ ๋‹จ๊ณ„ ๋” ๊ฐ”๋‹ค.

๊ธฐ์กด์—๋Š”

Long projectId;

์ด๋ ‡๊ฒŒ ๋‹จ์ˆœ ID๋กœ ๊ด€๋ฆฌํ–ˆ๋Š”๋ฐ

์ด๊ฑธ ๋ฐ”๊ฟจ๋‹ค.

@ManyToOne(fetch = FetchType.LAZY)
private Project project;

์ด๋ ‡๊ฒŒ ๋ฐ”๊พธ๋‹ˆ๊นŒ

Task โ†’ Project ๊ด€๊ณ„๊ฐ€ ๋ช…ํ™•ํ•ด์ง€๊ณ 
JPA ๊ธฐ์ค€์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์กฐํšŒ ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค


โ˜˜๏ธ 8. Repository๋„ ๊ฐ™์ด ๋ฐ”๋€๋‹ค

์ด ๊ตฌ์กฐ๊ฐ€ ๋˜๋ฉด์„œ

๋ฉ”์„œ๋“œ๋„ ๋ฐ”๋€๋‹ค.

๊ธฐ์กด:

findByProjectIdAndStatus(...)

๋ณ€๊ฒฝ:

findByProject_IdAndStatus(...)

์ด๊ฑด ๋‹จ์ˆœ ๋ฌธ๋ฒ•์ด ์•„๋‹ˆ๋ผ

โ†’ ๊ฐ์ฒด ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์กฐํšŒ๋กœ ๋ฐ”๋€ ๊ฒƒ


โ˜˜๏ธ 9. ์‘๋‹ต ๊ตฌ์กฐ๋„ ์ •๋ฆฌ

Entity ๊ทธ๋Œ€๋กœ ๋‚ด๋ ค์ฃผ๋Š” ๊ฑด ์•ˆ ๋งž์•„์„œ

TaskResponse๋„ ์ˆ˜์ •ํ–ˆ๋‹ค.

this.projectId = task.getProject().getId();
this.projectName = task.getProject().getName();

โ†’ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ๋‚ด๋ ค์ฃผ๋Š” ๊ตฌ์กฐ


โ˜˜๏ธ 10. ์˜ค๋Š˜ ์ •๋ฆฌ

์˜ค๋Š˜ ํ•œ ๊ฑธ ํ•œ ์ค„๋กœ ์ •๋ฆฌํ•˜๋ฉด ์ด๊ฑฐ๋‹ค.

์—ฌ๋Ÿฌ Task์— ํฉ์–ด์ ธ ์žˆ๋˜ ์ด๋ฒคํŠธ๋ฅผ ๋ชจ์•„์„œ
ํ”„๋กœ์ ํŠธ ์ „์ฒด ํ๋ฆ„์œผ๋กœ ์žฌ๊ตฌ์„ฑํ–ˆ๋‹ค


โ˜˜๏ธ 11. ํ˜„์žฌ ์ƒํƒœ

Project ๋„๋ฉ”์ธ โœ”๏ธ
Task โ†” Project ๊ด€๊ณ„ โœ”๏ธ
Default Project โœ”๏ธ
Task Timeline โœ”๏ธ
Project Timeline โœ”๏ธ

์ด์ œ๋Š”

โ†’ ํ๋ฆ„์„ ์กฐํšŒํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ
โ†’ ํ๋ฆ„์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ๊ฐ€ ๋๋‹ค


โ˜˜๏ธ 12. ๋‹ค์Œ ๋ชฉํ‘œ

์—ฌ๊ธฐ์„œ ๋์ด ์•„๋‹ˆ๋‹ค.

์ง€๊ธˆ์€ ํ๋ฆ„์„ ๋‚˜์—ด๋งŒ ํ•˜๊ณ  ์žˆ๋‹ค.

๋‹ค์Œ ๋‹จ๊ณ„๋Š” ์ด๊ฑฐ๋‹ค.

ํ”„๋กœ์ ํŠธ ๋‹จ์œ„ ํ†ต๊ณ„ (์–ผ๋งˆ๋‚˜ ์˜ค๋ž˜ ๊ฑธ๋ ธ๋Š”์ง€)
์ƒํƒœ๋ณ„ ์ฒด๋ฅ˜ ์‹œ๊ฐ„ ๋ถ„์„
๊ฐ€์žฅ ์˜ค๋ž˜ ๋ง‰ํžŒ ๊ตฌ๊ฐ„ ์ฐพ๊ธฐ

โ†’ ํ๋ฆ„์„ ํ•ด์„ํ•˜๋Š” ๋‹จ๊ณ„

๊ทธ๋ฆฌ๊ณ  ํ•œ ๋‹จ๊ณ„ ๋” ๋‚˜๊ฐ€๋ฉด

โ†’ Redis๋ฅผ ๋ถ™์—ฌ์„œ
โ†’ ๊ณ„์‚ฐ ๋น„์šฉ์ด ํฐ ์กฐํšŒ๋ฅผ ์บ์‹ฑํ•  ์˜ˆ์ •

์ฆ‰

๋‹จ์ˆœ ์กฐํšŒ โ†’ ํ๋ฆ„ ์ดํ•ด โ†’ ํ๋ฆ„ ๋ถ„์„ โ†’ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋กœ ํ™•์žฅํ•ด ๋‚˜๊ฐˆ ์˜ˆ์ •์ด๋‹ค

0๊ฐœ์˜ ๋Œ“๊ธ€