# ๐Ÿ“‹ ์„ค๊ณ„ ๋ฌธ์„œ: get_videos ์—”๋“œํฌ์ธํŠธ ์—…๋ฐ์ดํŠธ ## 1. ์š”๊ตฌ์‚ฌํ•ญ ์š”์•ฝ ### ๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ | # | ์š”๊ตฌ์‚ฌํ•ญ | ํ˜„์žฌ ์ƒํƒœ | ๋ณ€๊ฒฝ | |---|---------|----------|------| | 1 | current_user ์†Œ์œ  ํ”„๋กœ์ ํŠธ์˜ ์˜์ƒ๋งŒ ๋ฐ˜ํ™˜ | ๊ตฌํ˜„๋จ | ์œ ์ง€ | | 2 | status='completed', is_deleted=False ํ•„ํ„ฐ | ๊ตฌํ˜„๋จ | ์œ ์ง€ | | 3 | ๋™์ผ task_id ์ค‘ created_at ์ตœ์‹  ์˜์ƒ 1๊ฐœ๋งŒ ๋ฐ˜ํ™˜ | ๋ฏธ๊ตฌํ˜„ (์ „์ฒด ๋ฐ˜ํ™˜) | **์‹ ๊ทœ** | | 4 | created_at DESC ์ •๋ ฌ | ๊ตฌํ˜„๋จ | ์œ ์ง€ | | 5 | DEBUG ์ฟผ๋ฆฌ ์ œ๊ฑฐ | 6๊ฐœ DEBUG ์ฟผ๋ฆฌ ์กด์žฌ | **์‚ญ์ œ** | ### ๋น„๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ - ๊ธฐ์กด ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ธํ„ฐํŽ˜์ด์Šค(PaginatedResponse, PaginationParams) ์œ ์ง€ - ๊ธฐ์กด ์‘๋‹ต ์Šคํ‚ค๋งˆ(VideoListItem) ์œ ์ง€ - SQLAlchemy ๋น„๋™๊ธฐ + PostgreSQL ํ˜ธํ™˜ --- ## 2. ์„ค๊ณ„ ๊ฐœ์š” ### ํ˜„์žฌ ๋ฌธ์ œ์  1. **DEBUG ์ฟผ๋ฆฌ 6๊ฐœ** (lines 80~142): ์ „์ฒด Video ์ˆ˜, completed ์ˆ˜, is_deleted ์ˆ˜, ์ „์ฒด Project ์ˆ˜, ์‚ฌ์šฉ์ž Project ์ˆ˜, ์‚ฌ์šฉ์ž completed Video ์ˆ˜๋ฅผ ๋งค ์š”์ฒญ๋งˆ๋‹ค ์กฐํšŒ โ†’ ๋ถˆํ•„์š”ํ•œ DB ๋ถ€ํ•˜ 2. **task_id ์ค‘๋ณต ๋ฐ˜ํ™˜**: ๋™์ผ task_id์— ์žฌ์ƒ์„ฑ๋œ ์˜์ƒ์ด ์—ฌ๋Ÿฌ ๊ฐœ ์กด์žฌํ•  ๋•Œ ๋ชจ๋‘ ๋ฐ˜ํ™˜ ### ์„ค๊ณ„ ๋ฐฉํ–ฅ - DEBUG ์ฟผ๋ฆฌ 6๊ฐœ ์ „๋ฉด ์‚ญ์ œ - **์„œ๋ธŒ์ฟผ๋ฆฌ ๋ฐฉ์‹**์œผ๋กœ task_id๋ณ„ ์ตœ์‹  ์˜์ƒ ํ•„ํ„ฐ๋ง - ์ฟผ๋ฆฌ๋ฅผ count ์ฟผ๋ฆฌ + ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ 2๊ฐœ๋กœ ์ •๋ฆฌ (๊ธฐ์กด ๊ตฌ์กฐ ์œ ์ง€) --- ## 3. API ์„ค๊ณ„ ### ์—”๋“œํฌ์ธํŠธ ๋ณ€๊ฒฝ ์—†์Œ โ€” ๊ธฐ์กด ์ธํ„ฐํŽ˜์ด์Šค ์œ ์ง€ ``` GET /archive/videos/?page=1&page_size=10 ``` - **Method**: GET - **Auth**: Bearer Token (get_current_user) - **Query Params**: page (int, default=1), page_size (int, default=10, max=100) - **Response**: PaginatedResponse[VideoListItem] ### description ์—…๋ฐ์ดํŠธ ๋‚ด์šฉ ``` - ๋ณธ์ธ์ด ์†Œ์œ ํ•œ ํ”„๋กœ์ ํŠธ์˜ ์˜์ƒ๋งŒ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค. - status๊ฐ€ 'completed'์ธ ์˜์ƒ๋งŒ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค. - ๋™์ผ task_id์˜ ์˜์ƒ์ด ์—ฌ๋Ÿฌ ๊ฐœ์ธ ๊ฒฝ์šฐ, ๊ฐ€์žฅ ์ตœ๊ทผ์— ์ƒ์„ฑ๋œ ์˜์ƒ๋งŒ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค. - created_at ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ๋ฉ๋‹ˆ๋‹ค. ``` --- ## 4. ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ### ๊ธฐ์กด ๋ชจ๋ธ (๋ณ€๊ฒฝ ์—†์Œ) **Video** (app/video/models.py) | ์ปฌ๋Ÿผ | ํƒ€์ž… | ์šฉ๋„ | |------|------|------| | id | Integer (PK, autoincrement) | ๊ณ ์œ  ์‹๋ณ„์ž | | project_id | Integer (FK โ†’ project.id) | ํ”„๋กœ์ ํŠธ ์—ฐ๊ฒฐ | | task_id | String(36) | ์ž‘์—… ์‹๋ณ„์ž (์ค‘๋ณต ๊ฐ€๋Šฅ) | | status | String(50) | ์ฒ˜๋ฆฌ ์ƒํƒœ | | result_movie_url | String(2048) | ์˜์ƒ URL | | is_deleted | Boolean | ์†Œํ”„ํŠธ ์‚ญ์ œ | | created_at | DateTime | ์ƒ์„ฑ ์ผ์‹œ | **Project** (app/home/models.py) | ์ปฌ๋Ÿผ | ํƒ€์ž… | ์šฉ๋„ | |------|------|------| | id | Integer (PK) | ๊ณ ์œ  ์‹๋ณ„์ž | | user_uuid | String(36, FK โ†’ user.user_uuid) | ์†Œ์œ ์ž | | store_name | String | ์—…์ฒด๋ช… | | region | String | ์ง€์—ญ๋ช… | | is_deleted | Boolean | ์†Œํ”„ํŠธ ์‚ญ์ œ | ### ์ธ๋ฑ์Šค ํ™œ์šฉ - `idx_video_task_id`: task_id GROUP BY์— ํ™œ์šฉ - `idx_video_project_id`: JOIN ์กฐ๊ฑด์— ํ™œ์šฉ - `idx_video_is_deleted`: WHERE ํ•„ํ„ฐ์— ํ™œ์šฉ - `idx_project_user_uuid`: ์‚ฌ์šฉ์ž ์†Œ์œ  ํ•„ํ„ฐ์— ํ™œ์šฉ --- ## 5. ์„œ๋น„์Šค ๋ ˆ์ด์–ด ### ์ฟผ๋ฆฌ ์„ค๊ณ„ (ํ•ต์‹ฌ) ํ˜„์žฌ ์•„ํ‚คํ…์ฒ˜์—์„œ get_videos๋Š” ๋ผ์šฐํ„ฐ์—์„œ ์ง์ ‘ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์žˆ์Œ (๋ณ„๋„ ์„œ๋น„์Šค ๋ ˆ์ด์–ด ์—†์Œ). ์ด ํŒจํ„ด์„ ์œ ์ง€ํ•˜๋˜, ์ฟผ๋ฆฌ ๋กœ์ง๋งŒ ์ˆ˜์ •ํ•œ๋‹ค. #### 5.1 ์„œ๋ธŒ์ฟผ๋ฆฌ: task_id๋ณ„ ์ตœ์‹  Video ID ์ถ”์ถœ ```python from sqlalchemy import func, select # ์„œ๋ธŒ์ฟผ๋ฆฌ: ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์˜์ƒ ์ค‘, task_id๋ณ„ MAX(id)๋ฅผ ์ถ”์ถœ # (id๋Š” autoincrement์ด๋ฏ€๋กœ created_at ์ตœ์‹ ๊ณผ ๋™์ผ) latest_video_ids = ( select(func.max(Video.id).label("latest_id")) .join(Project, Video.project_id == Project.id) .where( Project.user_uuid == current_user.user_uuid, Video.status == "completed", Video.is_deleted == False, Project.is_deleted == False, ) .group_by(Video.task_id) .subquery() ) ``` **์„ค๊ณ„ ๊ทผ๊ฑฐ**: `Video.id`๋Š” autoincrement์ด๋ฏ€๋กœ ๋‚˜์ค‘์— ์ƒ์„ฑ๋œ ๋ ˆ์ฝ”๋“œ๊ฐ€ ํ•ญ์ƒ ๋” ํฐ id๋ฅผ ๊ฐ€์ง„๋‹ค. ๋”ฐ๋ผ์„œ `MAX(id)`๋Š” `created_at`์ด ๊ฐ€์žฅ ์ตœ์‹ ์ธ ๋ ˆ์ฝ”๋“œ์™€ ์ผ์น˜ํ•œ๋‹ค. Window Function(ROW_NUMBER) ๋Œ€๋น„ ์ฟผ๋ฆฌ๊ฐ€ ๋‹จ์ˆœํ•˜๊ณ  ์„ฑ๋Šฅ์ด ์šฐ์ˆ˜ํ•˜๋‹ค. #### 5.2 COUNT ์ฟผ๋ฆฌ (ํŽ˜์ด์ง€๋„ค์ด์…˜์šฉ) ```python count_query = ( select(func.count(Video.id)) .where(Video.id.in_(select(latest_video_ids.c.latest_id))) ) ``` #### 5.3 ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ ```python data_query = ( select(Video, Project) .join(Project, Video.project_id == Project.id) .where(Video.id.in_(select(latest_video_ids.c.latest_id))) .order_by(Video.created_at.desc()) .offset(offset) .limit(pagination.page_size) ) ``` #### 5.4 ์ „์ฒด ์ฟผ๋ฆฌ ํ๋ฆ„ ``` 1. latest_video_ids (์„œ๋ธŒ์ฟผ๋ฆฌ) โ†’ Video JOIN Project โ†’ WHERE: user_uuid, status, is_deleted ํ•„ํ„ฐ โ†’ GROUP BY task_id โ†’ MAX(id) 2. count_query โ†’ WHERE Video.id IN (latest_video_ids) โ†’ scalar count 3. data_query โ†’ Video JOIN Project โ†’ WHERE Video.id IN (latest_video_ids) โ†’ ORDER BY created_at DESC โ†’ OFFSET/LIMIT ``` ### ์ƒ์„ฑ๋˜๋Š” SQL (์ฐธ๊ณ ) ```sql -- ์„œ๋ธŒ์ฟผ๋ฆฌ SELECT MAX(v.id) AS latest_id FROM video v JOIN project p ON v.project_id = p.id WHERE p.user_uuid = :user_uuid AND v.status = 'completed' AND v.is_deleted = FALSE AND p.is_deleted = FALSE GROUP BY v.task_id; -- ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ SELECT v.*, p.* FROM video v JOIN project p ON v.project_id = p.id WHERE v.id IN (์œ„ ์„œ๋ธŒ์ฟผ๋ฆฌ) ORDER BY v.created_at DESC OFFSET :offset LIMIT :limit; ``` --- ## 6. ์Šคํ‚ค๋งˆ ### ๋ณ€๊ฒฝ ์—†์Œ โ€” ๊ธฐ์กด ์Šคํ‚ค๋งˆ ์œ ์ง€ **VideoListItem** (app/video/schemas/video_schema.py) ```python class VideoListItem(BaseModel): video_id: int store_name: Optional[str] region: Optional[str] task_id: str result_movie_url: Optional[str] created_at: Optional[datetime] ``` **PaginatedResponse[VideoListItem]** (app/utils/pagination.py) ```python { "items": [VideoListItem, ...], "total": int, "page": int, "page_size": int, "total_pages": int, "has_next": bool, "has_prev": bool } ``` --- ## 7. ํŒŒ์ผ ๊ตฌ์กฐ | ํŒŒ์ผ | ์ž‘์—… | ์„ค๋ช… | |------|------|------| | app/archive/api/routers/v1/archive.py | **์ˆ˜์ •** | get_videos ํ•จ์ˆ˜ ๋ฆฌํŒฉํ† ๋ง | **์ˆ˜์ •ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ:** - app/video/models.py (๋ณ€๊ฒฝ ์—†์Œ) - app/video/schemas/video_schema.py (๋ณ€๊ฒฝ ์—†์Œ) - app/utils/pagination.py (๋ณ€๊ฒฝ ์—†์Œ) - app/dependencies/pagination.py (๋ณ€๊ฒฝ ์—†์Œ) - app/home/models.py (๋ณ€๊ฒฝ ์—†์Œ) --- ## 8. ๊ตฌํ˜„ ์ˆœ์„œ ๊ฐœ๋ฐœ ์—์ด์ „ํŠธ(`/develop`)๊ฐ€ ๋”ฐ๋ผ์•ผ ํ•  ์ˆœ์„œ: ### Step 1: get_videos ํ•จ์ˆ˜ ์ˆ˜์ • 1. **DEBUG ์ฟผ๋ฆฌ ์‚ญ์ œ** (lines 80~142) - ์ „์ฒด Video ์ˆ˜ ์กฐํšŒ ์‚ญ์ œ - completed ์ƒํƒœ Video ์ˆ˜ ์กฐํšŒ ์‚ญ์ œ - is_deleted=False Video ์ˆ˜ ์กฐํšŒ ์‚ญ์ œ - ์ „์ฒด Project ์ˆ˜/์ƒ์„ธ ์กฐํšŒ ์‚ญ์ œ - ํ˜„์žฌ ์‚ฌ์šฉ์ž ์†Œ์œ  Project ์ˆ˜ ์กฐํšŒ ์‚ญ์ œ - ํ˜„์žฌ ์‚ฌ์šฉ์ž completed Video ์ˆ˜ ์กฐํšŒ ์‚ญ์ œ 2. **์„œ๋ธŒ์ฟผ๋ฆฌ ์ถ”๊ฐ€**: task_id๋ณ„ MAX(id) ์ถ”์ถœ - base_conditions๋ฅผ ์„œ๋ธŒ์ฟผ๋ฆฌ์˜ WHERE์ ˆ์— ์ ์šฉ 3. **COUNT ์ฟผ๋ฆฌ ์ˆ˜์ •**: Video.id IN (์„œ๋ธŒ์ฟผ๋ฆฌ) ์กฐ๊ฑด ์ ์šฉ 4. **๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ ์ˆ˜์ •**: Video.id IN (์„œ๋ธŒ์ฟผ๋ฆฌ) + ORDER BY + OFFSET/LIMIT 5. **์—”๋“œํฌ์ธํŠธ description ์—…๋ฐ์ดํŠธ**: "๋™์ผ task_id์˜ ๊ฐ€์žฅ ์ตœ๊ทผ ์˜์ƒ๋งŒ ๋ฐ˜ํ™˜" ๋ฌธ๊ตฌ ์ถ”๊ฐ€, ๊ธฐ์กด "์žฌ์ƒ์„ฑ๋œ ์˜์ƒ ํฌํ•จ ๋ชจ๋“  ์˜์ƒ์ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค" ๋ฌธ๊ตฌ ์‚ญ์ œ ### Step 2: ๋กœ๊น… ์ •๋ฆฌ - ๊ธฐ์กด DEBUG ๋กœ๊ทธ ์‚ญ์ œ - ํ•ต์‹ฌ ๋กœ๊ทธ๋งŒ ์œ ์ง€: START, SUCCESS, EXCEPTION --- ## 9. ์„ค๊ณ„ ๊ฒ€์ˆ˜ ๊ฒฐ๊ณผ ### ๊ฒ€์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - [x] **๊ธฐ์กด ํ”„๋กœ์ ํŠธ ํŒจํ„ด๊ณผ ์ผ๊ด€์„ฑ**: ๊ธฐ์กด ๋ผ์šฐํ„ฐ ์ง์ ‘ ์ฟผ๋ฆฌ ํŒจํ„ด ์œ ์ง€, PaginatedResponse.create() ํ™œ์šฉ - [x] **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์„ค๊ณ„**: async/await + AsyncSession ์œ ์ง€ - [x] **N+1 ์ฟผ๋ฆฌ ๋ฌธ์ œ**: JOIN์œผ๋กœ ํ•œ ๋ฒˆ์— ์กฐํšŒ, ์„œ๋ธŒ์ฟผ๋ฆฌ๋Š” IN์ ˆ๋กœ ๋‹จ์ผ ์ฟผ๋ฆฌ ์‹คํ–‰ - [x] **ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„**: ์ฝ๊ธฐ ์ „์šฉ ์ฟผ๋ฆฌ์ด๋ฏ€๋กœ ํŠธ๋žœ์žญ์…˜ ๋ถˆํ•„์š” (๊ธฐ์กด๊ณผ ๋™์ผ) - [x] **์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ „๋žต**: ๊ธฐ์กด try/except + HTTPException 500 ํŒจํ„ด ์œ ์ง€ - [x] **ํ™•์žฅ์„ฑ**: ์„œ๋ธŒ์ฟผ๋ฆฌ ๋ฐฉ์‹์€ ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด ํ™•์žฅ ์šฉ์ด - [x] **์ง๊ด€์  ๊ตฌ์กฐ**: ์„œ๋ธŒ์ฟผ๋ฆฌ(์ตœ์‹  ID ์ถ”์ถœ) โ†’ COUNT โ†’ DATA 3๋‹จ๊ณ„๋กœ ๋ช…ํ™• - [x] **SOLID ์ค€์ˆ˜**: ๋‹จ์ผ ์ฑ…์ž„(์˜์ƒ ๋ชฉ๋ก ์กฐํšŒ), ๊ธฐ์กด ์ธํ„ฐํŽ˜์ด์Šค ์œ ์ง€(OCP) ### ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ - ์„œ๋ธŒ์ฟผ๋ฆฌ `GROUP BY task_id`๋Š” `idx_video_task_id` ์ธ๋ฑ์Šค ํ™œ์šฉ - `Video.id IN (์„œ๋ธŒ์ฟผ๋ฆฌ)`๋Š” PK ์ธ๋ฑ์Šค๋กœ ๋น ๋ฅธ ์กฐํšŒ - ๊ธฐ์กด ๋Œ€๋น„ DEBUG ์ฟผ๋ฆฌ 6๊ฐœ ์‚ญ์ œ๋กœ DB ์š”์ฒญ ํšŸ์ˆ˜: 8ํšŒ โ†’ 2ํšŒ ### ๋Œ€์•ˆ ๊ฒ€ํ†  | ๋ฐฉ์‹ | ์žฅ์  | ๋‹จ์  | ์ฑ„ํƒ | |------|------|------|------| | **MAX(id) ์„œ๋ธŒ์ฟผ๋ฆฌ** | ๋‹จ์ˆœ, ๋น ๋ฆ„, DB ๋ฌด๊ด€ | id ์ˆœ์„œ = ์‹œ๊ฐ„ ์ˆœ์„œ ์ „์ œ | **์ฑ„ํƒ** | | ROW_NUMBER() ์œˆ๋„์šฐ ํ•จ์ˆ˜ | created_at ์ง์ ‘ ๊ธฐ์ค€ | ์ฟผ๋ฆฌ ๋ณต์žก, ์„œ๋ธŒ์ฟผ๋ฆฌ ๋ž˜ํ•‘ ํ•„์š” | ๋ฏธ์ฑ„ํƒ | | DISTINCT ON (PostgreSQL) | PostgreSQL ์ตœ์ ํ™” | DB ์ข…์†, ์ •๋ ฌ ์ œ์•ฝ | ๋ฏธ์ฑ„ํƒ | MAX(id) ์„œ๋ธŒ์ฟผ๋ฆฌ ์ฑ„ํƒ ๊ทผ๊ฑฐ: Video.id๋Š” autoincrement์ด๋ฏ€๋กœ `MAX(id)`๊ฐ€ `created_at` ์ตœ์‹  ๋ ˆ์ฝ”๋“œ์™€ ์ผ์น˜. ์ฟผ๋ฆฌ๊ฐ€ ๊ฐ€์žฅ ๋‹จ์ˆœํ•˜๊ณ  ๋ชจ๋“  RDBMS์—์„œ ๋™์ž‘. --- ## ๋‹ค์Œ ๋‹จ๊ณ„ ์„ค๊ณ„ ๊ฒ€ํ†  ์™„๋ฃŒ ํ›„ `/develop` ๋ช…๋ น์œผ๋กœ ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.