9.5 KiB
Design — todo-app (간단한 To-Do 앱)
PDCA Phase: Design · Feature:
todo-app· 작성일: 2026-06-15 선택 아키텍처: Option C — 실용 균형 · 첨부 다운로드: 서버 프록시 스트리밍 상위 문서: Plan
Context Anchor
| 항목 | 내용 |
|---|---|
| WHY | 사내 워크스페이스 샘플을 실제로 쓸 수 있는 최소 To-Do 앱으로 만들어 DB+S3 활용 패턴을 보여준다. |
| WHO | 로그인 없이 접근하는 단일 사용자/소규모 팀(단일 공용 목록). |
| RISK | 첨부파일 처리(크기/타입), schema 마이그레이션 누락, S3 키-DB 레코드 정합성. |
| SUCCESS | 브라우저에서 할 일 CRUD + 첨부 업로드/다운로드가 동작하고 npm run db:check/minio:check 통과. |
| SCOPE | IN: todo CRUD, 첨부 1개/항목, 웹 UI, REST API. OUT: 인증/멀티유저, 마감일/태그/검색, 실시간. |
1. Overview
기존 Express 앱(src/server.js)에 To-Do 도메인을 추가한다. 구조화 데이터는 Postgres(src/db.js pool),
첨부파일 바이너리는 MinIO/S3(src/s3.js client)에 저장하고 두 저장소를 S3 object key로 연결한다.
아키텍처 결정 (Option C — 실용 균형): route / repo / attachments 3개 모듈로 관심사를 분리하되, 별도 service 계층은 두지 않는다(라우트 핸들러가 repo+attachments를 조합). "간단한 앱" 목표에 맞는 최소한의 경계.
다운로드 결정 (서버 프록시 스트리밍): 브라우저는 앱 엔드포인트에만 요청하고, 서버가 GetObjectCommand로
S3 객체를 받아 응답 스트림으로 전달. 내부 MinIO 호스트(minio.bokdev.in) 노출 없이 환경 무관하게 동작.
→ presigned URL 불필요, @aws-sdk/s3-request-presigner 의존성 추가하지 않음.
2. 모듈 구조
src/
config.js (기존) 변경 없음
db.js (기존) pool, pingDb — 재사용
s3.js (기존) s3 client, pingS3 — 재사용. GetObject/PutObject/DeleteObject 추가 export
schema.js (신규) todo 테이블 멱등 생성 (앱 시작 시 ensureSchema 호출)
todos.repo.js (신규) DB CRUD 쿼리 (pool 사용)
attachments.js (신규) S3 put / get(stream) / delete 래퍼
routes.todos.js (신규) Express Router — REST 핸들러
server.js (수정) express.json/static 미들웨어, schema 부트스트랩, 라우터 마운트
public/
index.html (신규) 단일 페이지 웹 UI (vanilla JS fetch)
3. 데이터 모델
-- schema.js 에서 CREATE TABLE IF NOT EXISTS 로 멱등 실행
CREATE TABLE IF NOT EXISTS todo (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
title text NOT NULL,
done boolean NOT NULL DEFAULT false,
attachment_key text, -- S3 object key (없으면 NULL)
attachment_name text, -- 원본 파일명
attachment_type text, -- content-type
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_todo_created_at ON todo (created_at DESC);
gen_random_uuid()는 pgcrypto 확장이 필요. 미존재 환경 대비: schema.js에서CREATE EXTENSION IF NOT EXISTS pgcrypto를 먼저 시도하고, 권한 부족으로 실패하면 앱에서crypto.randomUUID()로 id를 생성해 INSERT (fallback).- S3 키 규칙:
todos/{todo_id}/{원본파일명}— 버킷은config.s3.bucket(coolify-user-data).
4. API 계약
| Method | Path | Request | Response (성공) | 비고 |
|---|---|---|---|---|
| GET | / |
— | text/html (index.html) |
정적 제공 |
| GET | /api/todos |
— | 200 { data: Todo[] } |
created_at DESC |
| POST | /api/todos |
multipart/form-data: title(필수), file(선택) |
201 { data: Todo } |
file 있으면 S3 업로드 후 key 기록 |
| PATCH | /api/todos/:id |
application/json: { done: boolean } |
200 { data: Todo } |
토글 |
| DELETE | /api/todos/:id |
— | 204 |
S3 첨부 먼저 삭제 후 DB row 삭제 |
| GET | /api/todos/:id/attachment |
— | 200 스트림(Content-Type, Content-Disposition) |
프록시 스트리밍, 첨부 없으면 404 |
공통 규약
- 성공 바디:
{ data: ... }, 에러 바디:{ error: string }. - 입력 검증:
title비어있으면400 { error }. 잘못된:id(uuid 형식 아님) →400. 없는 todo →404. - 업로드 제한: multer
limits.fileSize = 10MB. 초과 시413 { error }.
Todo 객체 형태
{
"id": "uuid",
"title": "string",
"done": false,
"attachment_name": "report.pdf | null",
"attachment_type": "application/pdf | null",
"hasAttachment": true,
"created_at": "ISO8601"
}
attachment_key는 응답에서 제외(내부 키 비노출), 대신hasAttachmentboolean 노출.
5. 처리 흐름 (핵심 시퀀스)
POST /api/todos (첨부 포함)
- multer가
title+file(메모리 버퍼) 파싱 → title 검증 - id 생성(또는 INSERT 후 반환된 id) → S3 key
todos/{id}/{filename}결정 attachments.put(key, buffer, contentType)→ S3 업로드- repo: INSERT (attachment_key/name/type 포함) → 201 반환
- 3단계 실패 시 DB INSERT 하지 않음(또는 보상 삭제) → 500
DELETE /api/todos/:id
- repo: SELECT attachment_key
- key 있으면
attachments.del(key)먼저 시도(실패는 로깅하되 계속) - repo: DELETE row → 204
GET /api/todos/:id/attachment
- repo: SELECT attachment_key/name/type → 없으면 404
attachments.getStream(key)→Content-Type,Content-Disposition: attachment; filename=...세팅- S3 Body 스트림을 res로 pipe
6. 에러 처리 & 정합성
| 상황 | 처리 |
|---|---|
| S3 업로드 성공, DB INSERT 실패 | 업로드한 객체 보상 삭제(best-effort) 후 500 |
| DB 삭제 전 S3 삭제 실패 | 경고 로깅 후 DB 삭제 진행(고아 객체는 허용, 데이터 무결성 우선) |
| pgcrypto 미존재 | crypto.randomUUID() fallback |
| 파일 크기 초과 | multer 에러 → 413 |
7. 의존성 변경
| 패키지 | 용도 | 비고 |
|---|---|---|
multer |
multipart 업로드 파싱(메모리 스토리지) | 신규 추가 |
@aws-sdk/client-s3 |
GetObjectCommand/PutObjectCommand/DeleteObjectCommand |
기존, 명령만 추가 사용 |
presigned 방식 미채택 →
@aws-sdk/s3-request-presigner추가하지 않음 (Plan 대비 변경점). 추가 후npm install로 lock 갱신.
8. Test Plan
L1 — API 엔드포인트 (서버 기동 시)
GET /api/todos→ 200,{ data: [] }형태POST /api/todos(title만) → 201, data.id 존재POST /api/todos(title 누락) → 400PATCH /api/todos/:id {done:true}→ 200, data.done=trueDELETE /api/todos/:id→ 204GET /api/todos/:bad-uuid/attachment→ 400/404
L2 — UI 액션 (수동 또는 Playwright)
- 페이지 로드 → 목록 렌더
- 입력+추가 → 목록에 새 항목, 입력창 비워짐
- 체크박스 토글 → done 반영
- 삭제 → 항목 사라짐
- 파일 첨부 추가 → 다운로드 링크 노출, 클릭 시 파일 받아짐
L3 — E2E 시나리오
- 첨부 포함 추가 → 다운로드 → 삭제까지 전체 흐름 무오류
- 연결 점검:
npm run db:check,npm run minio:check통과 (SC-05) - 컨테이너:
podman build+podman run --env-file .project-env동작 (SC-06)
9. Success Criteria 매핑
| SC | Design 반영 위치 |
|---|---|
| SC-01 (CRUD UI) | §2 routes.todos.js + public/index.html, §4 API |
| SC-02 (업로드→S3+DB key) | §5 POST 흐름, §3 데이터 모델 |
| SC-03 (다운로드) | §5 GET attachment, 프록시 스트리밍 |
| SC-04 (삭제 시 S3도 삭제) | §5 DELETE 흐름, §6 정합성 |
| SC-05 (db/minio check) | §8 L3 |
| SC-06 (podman) | §8 L3 |
10. 보안/규약 체크
- 비밀값: 전부 env(
config.js) 경유, 코드/커밋 금지 (CLAUDE.md 준수). - 외부 연결: db/s3/config 모듈만 경유.
- 입력 검증: title 길이/공백, uuid 형식, 파일 크기·(선택)타입.
- 내부 S3 키/호스트 비노출(프록시 + 응답에서 key 제외).
11. Implementation Guide
11.1 구현 순서
src/schema.js— ensureSchema (pgcrypto 시도 + CREATE TABLE)src/s3.js확장 — putObject/getObjectStream/deleteObject exportsrc/attachments.js— 키 생성 규칙 + put/getStream/delsrc/todos.repo.js— list/create/setDone/getById/removesrc/routes.todos.js— 6개 핸들러 + multer 미들웨어 + 검증src/server.js— express.json, static(public), ensureSchema 호출, 라우터 마운트public/index.html— fetch 기반 UIpackage.json— multer 추가,npm install
11.2 핵심 파일
src/routes.todos.js(가장 로직 밀집),src/attachments.js,public/index.html
11.3 Session Guide (Module Map)
| 모듈 키 | 범위 | 포함 파일 | 의존 |
|---|---|---|---|
module-1 (데이터 계층) |
schema + repo + s3/attachments | schema.js, s3.js(확장), attachments.js, todos.repo.js | 없음 |
module-2 (API 계층) |
라우트 + 서버 연결 + 의존성 | routes.todos.js, server.js, package.json | module-1 |
module-3 (UI) |
웹 프런트 | public/index.html | module-2 |
권장 세션 분할
- 1세션에 전부 가능(소규모). 분할 시:
--scope module-1→--scope module-2→--scope module-3.
12. 다음 단계
/pdca do todo-app (전체) 또는 /pdca do todo-app --scope module-1 (점진 구현).