-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmedication-persistence.json
More file actions
75 lines (75 loc) · 17.1 KB
/
Copy pathmedication-persistence.json
File metadata and controls
75 lines (75 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"project": "glpcare-medication-persistence",
"description": "투약 데이터 영속성 — Prisma 스키마 확장 + Fastify API + injection.tsx API 연동",
"codebase": "/Users/bugbookee/.paperclip/instances/default/projects/c130b9f3-5855-47c1-a128-b16f07ebdc69/12805932-2b62-4dff-93e9-39732f9e3926/_default",
"steps": [
{
"step": "design",
"role": "CTO",
"model": "opus",
"preset": ["backend-fastify", "frontend-rn"],
"prompt": "GLP-Care 투약 관리 백엔드 영속성 설계서를 작성하라.\n\n## 현재 상황\n- injection.tsx: 약(Medication), 스케줄(MedSchedule), 로그(MedLog)를 useState로만 관리 → 앱 재시작 시 데이터 소실\n- 현재 Prisma Medication 모델은 단순함 (name, frequency, timesPerDay만 있음)\n- 서버는 Fastify 5 + Prisma + PostgreSQL\n\n## 현재 Prisma Medication 모델\n```prisma\nmodel Medication {\n id String @id @default(uuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n name String\n frequency String\n timesPerDay Int?\n timesPerWeek Int?\n timeOfDay String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n```\n\n## injection.tsx의 TypeScript 타입\n```typescript\ntype MedForm = 'oral' | 'injection' | 'other';\ntype FreqType = 'daily' | 'weekly';\ntype TimeSlot = 'morning' | 'lunch' | 'evening' | 'bedtime';\ntype MealCondition = 'fasting' | 'before' | 'after' | 'any';\ntype LogStatus = 'taken' | 'missed' | 'pending';\n\ninterface Medication {\n id: string;\n name: string;\n form: MedForm;\n dosage: string;\n frequency: FreqType;\n frequencyDays: string[]; // ['월', '수', '금']\n schedules: MedSchedule[];\n condition: MealCondition;\n memo: string;\n colorIndex: number;\n isActive: boolean;\n createdAt: string;\n}\n\ninterface MedSchedule {\n id: string;\n timeSlot: TimeSlot;\n timeDetail: string; // '08:00'\n}\n\ninterface MedLog {\n id: string;\n medicationId: string;\n scheduleId: string;\n date: string; // 'YYYY-MM-DD'\n status: LogStatus;\n checkedAt: string | null;\n}\n```\n\n## 설계 요구사항\n\n1. **Prisma 스키마 확장**\n - Medication 모델 확장 (form, dosage, frequencyDays, condition, memo, colorIndex, isActive 필드 추가)\n - MedicationSchedule 모델 추가\n - MedicationLog 모델 추가\n - User 모델의 medications 관계 유지\n\n2. **API 엔드포인트 설계**\n - GET /api/medications — 내 약 목록 (schedules 포함)\n - POST /api/medications — 약 등록\n - PUT /api/medications/:id — 약 수정\n - DELETE /api/medications/:id — 약 삭제\n - GET /api/medication-logs?date=YYYY-MM-DD — 날짜별 로그\n - GET /api/medication-logs?startDate=&endDate= — 기간별 로그\n - PUT /api/medication-logs/:medicationId/:scheduleId/:date — 로그 토글 (taken/pending)\n\n3. **injection.tsx API 연동 전략**\n - useState → useEffect로 초기 로드\n - toggleCheck → API PUT 호출\n - handleAddMedication → API POST 호출\n - handleDeleteMedication → API DELETE 호출\n - 낙관적 업데이트 (UI 먼저, 실패 시 롤백) 또는 단순 API 후 리로드\n - 어떤 전략이 더 나은지 판단해서 명시\n\n## 출력 형식\n각 섹션별:\n1. Prisma 스키마 전체 변경안 (기존 모델 포함, 새 모델 추가)\n2. API 엔드포인트별 Request/Response 스펙\n3. medication.ts 라우트 파일 구조\n4. injection.tsx 연동 전략 (어떤 훅/상태 구조로 바꾸는지)\n5. 마이그레이션 명령어\n",
"output": "output/medication-persistence-design.md"
},
{
"step": "schema",
"role": "Developer",
"model": "codex",
"preset": "backend-fastify",
"prompt": "GLP-Care Prisma 스키마를 수정하라. 설계서에 따라 Medication 모델 확장 및 MedicationSchedule, MedicationLog 모델을 추가한다.\n\n## 수정 파일\nprisma/schema.prisma\n\n## 규칙\n- 기존 모델 (User, RefreshToken, MealLog, DailySummary, WeeklyReport, AIAnalysisQuota, Injection, FoodCache, WeightLog 등) 전부 유지\n- Medication 모델에 다음 필드 추가:\n - form: String @default(\"oral\") — 'oral' | 'injection' | 'other'\n - dosage: String @default(\"\")\n - frequencyType: String @default(\"daily\") — 'daily' | 'weekly'\n - frequencyDays: String[] — 요일 배열 (PostgreSQL text[])\n - condition: String @default(\"any\") — 'fasting' | 'before' | 'after' | 'any'\n - memo: String @default(\"\")\n - colorIndex: Int @default(0)\n - isActive: Boolean @default(true)\n - schedules: MedicationSchedule[]\n - logs: MedicationLog[]\n- MedicationSchedule 모델 추가:\n - id: String @id @default(uuid())\n - medicationId: String\n - medication: Medication 관계\n - timeSlot: String — 'morning' | 'lunch' | 'evening' | 'bedtime'\n - timeDetail: String — 'HH:MM'\n - createdAt: DateTime\n - @@index([medicationId])\n- MedicationLog 모델 추가:\n - id: String @id @default(uuid())\n - userId: String\n - user: User 관계\n - medicationId: String\n - medication: Medication 관계\n - scheduleId: String\n - schedule: MedicationSchedule 관계\n - date: String — 'YYYY-MM-DD'\n - status: String @default(\"pending\") — 'taken' | 'missed' | 'pending'\n - checkedAt: DateTime?\n - createdAt: DateTime\n - updatedAt: DateTime\n - @@unique([medicationId, scheduleId, date])\n - @@index([userId, date])\n- User 모델에 medicationLogs MedicationLog[] 관계 추가\n\n## 출력 형식 (필수)\n// === prisma/schema.prisma\n[전체 schema.prisma 내용]",
"input": ["design"],
"files": ["prisma/schema.prisma"],
"output": "code"
},
{
"step": "route",
"role": "Developer",
"model": "codex",
"preset": "backend-fastify",
"prompt": "GLP-Care 투약 관리 API 라우트 파일을 새로 작성하라.\n\n## 생성 파일\nsrc/routes/medication.ts\n\n## injection.ts 패턴 참고 (같은 스타일로 작성)\n- FastifyPluginAsync 타입 사용\n- z.object로 입력 검증\n- fastify.authenticate preHandler 사용\n- userId = request.user.sub\n- 에러 응답: { success: false, message: string }\n- 성공 응답: { success: true, data: ... }\n\n## 구현할 엔드포인트\n\n### GET /api/medications\n- 내 약 목록 (isActive 기준 정렬, schedules 포함)\n- 응답: { success: true, data: Medication[] } (schedules 중첩 포함)\n\n### POST /api/medications\n- 요청 body: { name, form, dosage, frequencyType, frequencyDays, schedules: [{timeSlot, timeDetail}], condition, memo, colorIndex }\n- Medication + MedicationSchedule를 Prisma transaction으로 함께 생성\n- 응답: 생성된 Medication (schedules 포함)\n\n### PUT /api/medications/:id\n- 요청 body: 위와 동일 (모두 optional)\n- 기존 schedules 전부 삭제 후 새로 insert (단순화)\n- 응답: 수정된 Medication (schedules 포함)\n\n### DELETE /api/medications/:id\n- Cascade: MedicationLog도 함께 삭제\n- 응답: { success: true, data: { id } }\n\n### GET /api/medication-logs\n- query: date(YYYY-MM-DD 단일) 또는 startDate+endDate (기간)\n- 내 userId의 로그만 반환\n- 응답: MedicationLog[]\n\n### PUT /api/medication-logs/:medicationId/:scheduleId/:date\n- taken이면 pending으로, pending이면 taken으로 토글\n- upsert 사용 (없으면 생성)\n- 응답: 업데이트된 MedicationLog\n\n## 출력 형식 (필수)\n// === src/routes/medication.ts\n[전체 코드]",
"input": ["design", "schema"],
"files": ["src/routes/medication.ts"],
"output": "code"
},
{
"step": "register-route",
"role": "Developer",
"model": "codex",
"preset": "backend-fastify",
"prompt": "src/app.ts에 medication 라우트를 등록하라.\n\n기존 패턴:\n```typescript\nimport injectionRoutes from './routes/injection.js';\n...\napp.register(injectionRoutes);\n```\n\n동일하게:\n```typescript\nimport medicationRoutes from './routes/medication.js';\n...\napp.register(medicationRoutes);\n```\n\ninjectionRoutes import/register 바로 다음 줄에 추가.\n\n## 출력 형식 (필수)\n// === src/app.ts\n[전체 코드]",
"input": ["route"],
"files": ["src/app.ts"],
"output": "code"
},
{
"step": "mobile-api",
"role": "Developer",
"model": "codex",
"preset": "frontend-rn",
"prompt": "mobile/src/lib/api.ts에 투약 관리 API 함수들을 추가하라.\n\n기존 패턴 (createInjection 참고):\n```typescript\nexport async function createInjection(payload: {...}): Promise<unknown> {\n const res = await wrappedFetch(`${API_BASE_URL}/api/injection`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n });\n return parseJsonOrThrow<unknown>(res, 'create injection failed');\n}\n```\n\n## 추가할 타입\n```typescript\nexport interface MedicationScheduleAPI {\n id: string;\n medicationId: string;\n timeSlot: string;\n timeDetail: string;\n}\n\nexport interface MedicationAPI {\n id: string;\n name: string;\n form: string;\n dosage: string;\n frequencyType: string;\n frequencyDays: string[];\n schedules: MedicationScheduleAPI[];\n condition: string;\n memo: string;\n colorIndex: number;\n isActive: boolean;\n createdAt: string;\n}\n\nexport interface MedicationLogAPI {\n id: string;\n medicationId: string;\n scheduleId: string;\n date: string;\n status: string;\n checkedAt: string | null;\n}\n\nexport interface CreateMedicationInput {\n name: string;\n form: string;\n dosage: string;\n frequencyType: string;\n frequencyDays: string[];\n schedules: { timeSlot: string; timeDetail: string }[];\n condition: string;\n memo: string;\n colorIndex: number;\n}\n```\n\n## 추가할 함수\n- fetchMedications(): Promise<MedicationAPI[]>\n- createMedication(input: CreateMedicationInput): Promise<MedicationAPI>\n- updateMedication(id: string, input: Partial<CreateMedicationInput>): Promise<MedicationAPI>\n- deleteMedication(id: string): Promise<void>\n- fetchMedicationLogs(params: { date?: string; startDate?: string; endDate?: string }): Promise<MedicationLogAPI[]>\n- toggleMedicationLog(medicationId: string, scheduleId: string, date: string): Promise<MedicationLogAPI>\n\n파일 끝에 추가. 기존 코드는 절대 수정하지 않는다.\n\n## 출력 형식 (필수)\n// === mobile/src/lib/api.ts\n[전체 코드]",
"input": ["route"],
"files": ["mobile/src/lib/api.ts"],
"output": "code"
},
{
"step": "injection-tsx",
"role": "Developer",
"model": "codex",
"preset": "frontend-rn",
"prompt": "mobile/app/(tabs)/injection.tsx를 수정하라. useState 기반 데이터를 API 연동으로 교체한다.\n\n## 핵심 변경사항\n\n### 1. import 추가\n```typescript\nimport {\n fetchMedications,\n createMedication,\n updateMedication,\n deleteMedication,\n fetchMedicationLogs,\n toggleMedicationLog,\n type MedicationAPI,\n type MedicationLogAPI,\n} from '../../src/lib/api';\nimport { useCallback, useEffect } from 'react';\n```\n\n### 2. InjectionScreen 상태 관리 변경\n```typescript\n// 기존\nconst [medications, setMedications] = useState<Medication[]>([]);\nconst [logs, setLogs] = useState<MedLog[]>([]);\n\n// 변경 후\nconst [medications, setMedications] = useState<Medication[]>([]);\nconst [logs, setLogs] = useState<MedLog[]>([]);\nconst [loading, setLoading] = useState(true);\nconst [error, setError] = useState<string | null>(null);\n```\n\n### 3. API → local state 변환 함수 추가\n```typescript\nfunction apiToMedication(m: MedicationAPI): Medication {\n return {\n id: m.id,\n name: m.name,\n form: m.form as MedForm,\n dosage: m.dosage,\n frequency: m.frequencyType as FreqType,\n frequencyDays: m.frequencyDays,\n schedules: m.schedules.map(s => ({ id: s.id, timeSlot: s.timeSlot as TimeSlot, timeDetail: s.timeDetail })),\n condition: m.condition as MealCondition,\n memo: m.memo,\n colorIndex: m.colorIndex,\n isActive: m.isActive,\n createdAt: m.createdAt,\n };\n}\n\nfunction apiToLog(l: MedicationLogAPI): MedLog {\n return {\n id: l.id,\n medicationId: l.medicationId,\n scheduleId: l.scheduleId,\n date: l.date,\n status: l.status as LogStatus,\n checkedAt: l.checkedAt,\n };\n}\n```\n\n### 4. useEffect로 초기 데이터 로드\n```typescript\nconst loadData = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const [meds, logsData] = await Promise.all([\n fetchMedications(),\n fetchMedicationLogs({\n startDate: getDateKey(new Date(Date.now() - 7 * 86400000)),\n endDate: getDateKey(new Date(Date.now() + 7 * 86400000)),\n }),\n ]);\n setMedications(meds.map(apiToMedication));\n setLogs(logsData.map(apiToLog));\n } catch (err) {\n setError('데이터를 불러오지 못했습니다.');\n } finally {\n setLoading(false);\n }\n}, []);\n\nuseEffect(() => { loadData(); }, [loadData]);\n```\n\n### 5. toggleCheck → API 호출\n```typescript\nconst toggleCheck = async (medicationId: string, scheduleId: string, date = getDateKey(new Date())) => {\n // 기존 로컬 토글 로직은 유지하되, API 호출 추가\n // 낙관적 업데이트: 로컬 먼저 업데이트 후 API 호출\n // API 실패 시 error 알림 (롤백은 loadData로)\n try {\n const updatedLog = await toggleMedicationLog(medicationId, scheduleId, date);\n setLogs(prev => {\n const filtered = prev.filter(l => !(l.medicationId === medicationId && l.scheduleId === scheduleId && l.date === date));\n return [...filtered, apiToLog(updatedLog)];\n });\n } catch (err) {\n Alert.alert('오류', '복약 기록 업데이트에 실패했습니다.');\n }\n};\n```\n단, C-5 체크 해제 확인 Alert는 기존대로 유지.\n\n### 6. handleAddMedication → API 호출\n```typescript\nconst handleAddMedication = async (draft: MedDraft) => {\n if (editingMedicationId) {\n const updated = await updateMedication(editingMedicationId, {\n name: draft.name, form: draft.form, dosage: draft.dosage,\n frequencyType: draft.frequency, frequencyDays: draft.frequencyDays,\n schedules: draft.schedules.map(s => ({ timeSlot: s.timeSlot, timeDetail: s.timeDetail })),\n condition: draft.condition, memo: draft.memo,\n });\n setMedications(prev => prev.map(m => m.id === editingMedicationId ? apiToMedication(updated) : m));\n setEditingMedicationId(null);\n setScreen('main');\n } else {\n const created = await createMedication({\n name: draft.name, form: draft.form, dosage: draft.dosage,\n frequencyType: draft.frequency, frequencyDays: draft.frequencyDays,\n schedules: draft.schedules.map(s => ({ timeSlot: s.timeSlot, timeDetail: s.timeDetail })),\n condition: draft.condition, memo: draft.memo,\n colorIndex: medications.length % COLORS.length,\n });\n setMedications(prev => [...prev, apiToMedication(created)]);\n // C-3 연속 등록 Alert 유지\n Alert.alert('등록 완료', '다른 약도 등록하시겠어요?', [\n { text: '완료', onPress: () => setScreen('main') },\n { text: '계속 추가', onPress: () => setScreen('add') },\n ]);\n }\n};\n```\n\n### 7. handleDeleteMedication → API 호출\n```typescript\nconst handleDeleteMedication = async (id: string) => {\n await deleteMedication(id);\n setMedications(prev => prev.filter(m => m.id !== id));\n setLogs(prev => prev.filter(l => l.medicationId !== id));\n};\n```\n\n### 8. 로딩/에러 화면\n- loading === true이면 간단한 로딩 뷰 (ActivityIndicator + '불러오는 중...')\n- error !== null이면 에러 뷰 (재시도 버튼 → loadData())\n\n## 절대 규칙\n- 기존 20개 UX 이슈 수정사항 (C-1~B-9) 전부 유지\n- NativeWind className 절대 금지\n- StyleSheet.create 사용\n- 기존 MainScreen/ManageScreen/AddScreen/CalendarView 컴포넌트 구조 유지\n\n## 출력 형식 (필수)\n// === mobile/app/(tabs)/injection.tsx\n[전체 코드]",
"input": ["mobile-api"],
"files": ["mobile/app/(tabs)/injection.tsx"],
"output": "code"
},
{
"step": "review",
"role": "Reviewer",
"model": "sonnet",
"preset": ["backend-fastify", "frontend-rn", "reviewer"],
"prompt": "GLP-Care 투약 영속성 구현 코드를 리뷰하라.\n\n## 체크리스트\n\n### 백엔드\n- Prisma 스키마: MedicationSchedule, MedicationLog 모델 올바른가\n- 모든 엔드포인트에 authenticate 미들웨어 있는가\n- userId 필터 모든 쿼리에 있는가\n- transaction 사용 (POST medication — schedule 동시 생성)\n- zod 검증 있는가\n- 에러 응답 표준화 ({ success, message })\n\n### 프론트엔드\n- className 없는가\n- DateTimePicker 없는가\n- API 실패 시 에러 처리 있는가\n- C-5 체크 해제 Alert 유지되는가\n- C-3 연속 등록 Alert 유지되는가\n- 기존 20개 UX 수정사항 유지되는가\n\n### 보안\n- 다른 유저의 medication 접근 불가한가\n- SQL injection 없는가 (Prisma만 사용)\n\n### 새 버그\n- apiToMedication 변환 함수 오류 없는가\n- toggleCheck에서 C-5 Alert가 API 호출과 충돌하는가\n",
"input": ["schema", "route", "mobile-api", "injection-tsx"],
"output": "output/medication-persistence-review.md",
"pass": 8
}
]
}