From 3f758cf88769a3d9b3af67244d145cd98105a937 Mon Sep 17 00:00:00 2001 From: dltmdrb667 Date: Sun, 21 Jun 2026 08:33:09 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=8B=AC=EB=A0=A5=20=EC=9B=94?= =?UTF-8?q?=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EA=B0=90=EC=A0=95?= =?UTF-8?q?=EC=9D=BC=EA=B8=B0=20=EB=82=A0=EC=A7=9C=EB=B3=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 4 +- src/app/(page)/calendar/page.module.scss | 19 ++- src/app/(page)/calendar/page.tsx | 31 ++++- src/app/api/calendar/.gitkeep | 0 src/app/api/calendar/route.ts | 110 ++++++++++++++++++ src/app/api/diary/route.ts | 70 +++++++++++ src/app/api/disputes/route.ts | 38 +++--- .../calendar/CalendarView.module.scss | 9 +- src/components/calendar/CalendarView.tsx | 65 +++++++---- .../calendar/RecordList.module.scss | 48 ++++++++ src/components/calendar/RecordList.tsx | 65 +++++------ src/components/diary/EmotionDiaryList.tsx | 33 ++---- src/components/ui/CategoryIcon.module.scss | 22 ++++ src/components/ui/CategoryIcon.tsx | 25 ++-- src/domains/calendar/.gitkeep | 0 src/domains/calendar/calendar.api.ts | 9 ++ src/domains/calendar/calendar.hooks.ts | 17 +++ src/domains/diary/diary.api.ts | 9 ++ src/domains/diary/diary.hooks.ts | 17 +++ src/domains/dispute/dispute.api.ts | 13 ++- src/domains/dispute/dispute.hooks.ts | 61 ++++++---- src/types/calendar.ts | 21 ++++ src/types/diary.ts | 2 +- 23 files changed, 545 insertions(+), 143 deletions(-) delete mode 100644 src/app/api/calendar/.gitkeep create mode 100644 src/app/api/calendar/route.ts create mode 100644 src/app/api/diary/route.ts create mode 100644 src/components/ui/CategoryIcon.module.scss delete mode 100644 src/domains/calendar/.gitkeep create mode 100644 src/domains/calendar/calendar.api.ts create mode 100644 src/domains/calendar/calendar.hooks.ts create mode 100644 src/domains/diary/diary.api.ts create mode 100644 src/domains/diary/diary.hooks.ts create mode 100644 src/types/calendar.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cf89f0a..9e93ffc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -617,7 +617,7 @@ enum ai_error_type { } enum ai_log_type { - ai_chat + ai_room judgement } @@ -648,7 +648,7 @@ enum shop_item_type { } enum statistics_source_type { - ai_chat + ai_room one_to_one_judgement combined } diff --git a/src/app/(page)/calendar/page.module.scss b/src/app/(page)/calendar/page.module.scss index a160387..abc4426 100644 --- a/src/app/(page)/calendar/page.module.scss +++ b/src/app/(page)/calendar/page.module.scss @@ -2,14 +2,27 @@ @use '@/styles/abstracts/mixins' as typo; @use '@/styles/abstracts/functions' as fn; +.calendarWrapper { + position: relative; +} + +.calendarOverlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.6); +} + .summary { display: flex; align-items: center; background-color: var(--bg-surface-soft); justify-content: space-between; color: v.$color-primary-700; - margin-top: 1rem; - padding: 0.75rem 1.875rem; + margin-top: fn.r(16); + padding: fn.r(12) fn.r(30); &__label, &__count { @@ -34,7 +47,7 @@ align-items: center; justify-content: space-between; width: fn.r(115); - height: fn.r(48);; + height: fn.r(48); margin-left: auto; margin-right: fn.r(20); margin-bottom: fn.r(24); diff --git a/src/app/(page)/calendar/page.tsx b/src/app/(page)/calendar/page.tsx index c2f731b..9783fff 100644 --- a/src/app/(page)/calendar/page.tsx +++ b/src/app/(page)/calendar/page.tsx @@ -1,12 +1,15 @@ 'use client'; import { useState } from 'react'; +import { useRouter } from 'next/navigation'; import dayjs from 'dayjs'; import CalendarView from '@/components/calendar/CalendarView'; import styles from '@/app/(page)/calendar/page.module.scss'; import EmotionDiaryList from '@/components/diary/EmotionDiaryList'; import RecordList from '@/components/calendar/RecordList'; import Tabs from '@/components/ui/Tabs'; +import { useCalendarRecords } from '@/domains/calendar/calendar.hooks'; +import Spinner from '@/components/ui/Spinner'; import type { DiaryTab } from '@/types/diary'; const tabItems = [ @@ -15,16 +18,38 @@ const tabItems = [ ] satisfies { id: DiaryTab; label: string }[]; export default function Calendar() { + const router = useRouter(); const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD')); const [activeTab, setActiveTab] = useState('emotion'); + const currentYear = dayjs(selectedDate).year(); + const currentMonth = dayjs(selectedDate).month() + 1; // dayjs month는 0부터 시작 + const { data: calendarData, isLoading } = useCalendarRecords(currentYear, currentMonth); + + const monthlyDisputeCount = calendarData?.records.reduce( + (sum, r) => sum + (r.dispute?.count ?? 0), 0 + ) ?? 0; + return (
- +
+ + {isLoading && ( +
+ +
+ )} +
이달 화해횟수
-
2번
+
+ {isLoading ? '불러오는 중...' : `${monthlyDisputeCount}번`} +
@@ -36,7 +61,7 @@ export default function Calendar() {
{activeTab === 'emotion' && ( - diff --git a/src/app/api/calendar/.gitkeep b/src/app/api/calendar/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts new file mode 100644 index 0000000..74cca4c --- /dev/null +++ b/src/app/api/calendar/route.ts @@ -0,0 +1,110 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/db' +import { getSessionUserId } from '@/lib/auth/session' +import type { ApiResponse } from '@/types/common' +import type { CalendarMonthResponse, CalendarRecordItem } from '@/types/calendar' + +// GET /api/calendar?year=2026&month=6 +// 해당 월의 날짜별 감정일기/사건 요약 반환 (달력 마킹용) +export async function GET(request: NextRequest) { + const session = await getServerSession(authOptions) + const userId = getSessionUserId(session) + if (!userId) { + return NextResponse.json( + { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, + { status: 401 }, + ) + } + + const { searchParams } = new URL(request.url) + const year = parseInt(searchParams.get('year') ?? '', 10) + const month = parseInt(searchParams.get('month') ?? '', 10) + + if (isNaN(year) || isNaN(month) || month < 1 || month > 12) { + return NextResponse.json( + { success: false, error: { code: 'VALIDATION_ERROR', message: 'year, month가 올바르지 않습니다.' } }, + { status: 400 }, + ) + } + + // 해당 월의 시작일(1일 00:00)과 종료일(다음달 1일 00:00) 계산 + const startDate = new Date(year, month - 1, 1) + const endDate = new Date(year, month, 1) + + try { + // 두 쿼리를 병렬로 실행해 응답 시간 단축 + const [diaries, disputes] = await Promise.all([ + + // 감정일기: 해당 월 전체 조회 (diaryDate + emotionType만 선택) + // 최신순 정렬 → 날짜별 첫 번째 항목이 그 날의 최신 감정 + prisma.emotionDiary.findMany({ + where: { + userId, + diaryDate: { gte: startDate, lt: endDate }, + deletedAt: null, + }, + select: { diaryDate: true, emotionType: true }, + orderBy: { createdAt: 'desc' }, + }), + + // 사건: 종료된(CLOSED) 사건만 조회 (createdAt + categoryGroup만 선택) + // participants.some으로 내가 참여한 사건만 필터링 + prisma.dispute.findMany({ + where: { + deletedAt: null, + status: 'CLOSED', + createdAt: { gte: startDate, lt: endDate }, + participants: { some: { userId } }, + }, + select: { createdAt: true, categoryGroup: true }, + orderBy: { createdAt: 'desc' }, + }), + ]) + + // 감정일기 날짜별 그루핑 + // Map key: "YYYY-MM-DD", value: { count, emotion } + // 이미 최신순 정렬이므로 날짜 첫 등장 시 저장한 emotion이 그 날의 최신 emotion + const diaryMap = new Map() + for (const d of diaries) { + const date = d.diaryDate.toISOString().slice(0, 10) + if (!diaryMap.has(date)) { + diaryMap.set(date, { count: 0, emotion: d.emotionType ?? 'neutral' }) + } + diaryMap.get(date)!.count++ + } + + // 사건 날짜별 그루핑 + // createdAt 기준으로 날짜 추출 → 동일 방식으로 최신 category 저장 + const disputeMap = new Map() + for (const d of disputes) { + const date = d.createdAt.toISOString().slice(0, 10) + if (!disputeMap.has(date)) { + disputeMap.set(date, { count: 0, category: d.categoryGroup.toLowerCase() }) + } + disputeMap.get(date)!.count++ + } + + // 두 Map의 날짜를 합쳐서 중복 없이 전체 날짜 목록 생성 + const allDates = new Set([...diaryMap.keys(), ...disputeMap.keys()]) + + // 각 날짜별로 diary/dispute 요약 조합 + // 둘 중 하나만 있는 날짜는 없는 쪽을 null로 처리 + const records: CalendarRecordItem[] = Array.from(allDates).map((date) => ({ + date, + diary: diaryMap.get(date) ?? null, + dispute: disputeMap.get(date) ?? null, + })) + + return NextResponse.json>({ + success: true, + data: { year, month, records }, + }) + } catch { + return NextResponse.json( + { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { status: 500 }, + ) + } +} diff --git a/src/app/api/diary/route.ts b/src/app/api/diary/route.ts new file mode 100644 index 0000000..bee85b8 --- /dev/null +++ b/src/app/api/diary/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/db' +import { getSessionUserId } from '@/lib/auth/session' +import type { ApiResponse } from '@/types/common' +import type { DiaryItem } from '@/types/diary' + +// GET /api/diary?date=2026-06-16 +// 특정 날짜의 감정일기 목록 조회. 최신순 정렬. 본인 데이터만 반환 +export async function GET(request: NextRequest) { + const session = await getServerSession(authOptions) + const userId = getSessionUserId(session) + if (!userId) { + return NextResponse.json( + { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, + { status: 401 }, + ) + } + + const { searchParams } = new URL(request.url) + const date = searchParams.get('date') + + if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { + return NextResponse.json( + { success: false, error: { code: 'VALIDATION_ERROR', message: 'date 파라미터가 필요합니다. (YYYY-MM-DD)' } }, + { status: 400 }, + ) + } + + const targetDate = new Date(date) + const nextDate = new Date(targetDate) + nextDate.setDate(nextDate.getDate() + 1) + + try { + const diaries = await prisma.emotionDiary.findMany({ + where: { + userId, + diaryDate: { gte: targetDate, lt: nextDate }, + deletedAt: null, + }, + select: { + id: true, + title: true, + content: true, + diaryDate: true, + emotionType: true, + }, + orderBy: { createdAt: 'desc' }, + }) + + const items: DiaryItem[] = diaries.map((d) => ({ + id: d.id, + title: d.title ?? '', + content: d.content, + date: d.diaryDate.toISOString().slice(0, 10), + emotionType: (d.emotionType ?? 'neutral') as DiaryItem['emotionType'], + })) + + return NextResponse.json>({ + success: true, + data: { items }, + }) + } catch { + return NextResponse.json( + { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { status: 500 }, + ) + } +} \ No newline at end of file diff --git a/src/app/api/disputes/route.ts b/src/app/api/disputes/route.ts index b78dd09..126c4c6 100644 --- a/src/app/api/disputes/route.ts +++ b/src/app/api/disputes/route.ts @@ -68,12 +68,14 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const rawCategory = searchParams.get('categoryGroup') + const date = searchParams.get('date') + const rawStatus = searchParams.get('status') // URL 쿼리 파라미터는 문자열로 전달되므로 "true" 문자열과 비교 const active = searchParams.get('active') === 'true' - const completed = searchParams.get('completed') === 'true' const page = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10)) const limit = Math.min(50, Math.max(1, parseInt(searchParams.get('limit') ?? '20', 10))) + if (rawCategory !== null && !VALID_CATEGORY_GROUPS.includes(rawCategory as CategoryGroup)) { return NextResponse.json( { @@ -98,21 +100,25 @@ export async function GET(request: NextRequest) { deletedAt: null, participants: { some: { userId } }, ...(rawCategory ? { categoryGroup: rawCategory.toUpperCase() as PrismaCategoryGroup } : {}), - // active=true: 진행중 상태만 필터링 (draft, judged, closed 등 제외) - ...(active - ? { - status: { - in: [ - DisputeStatus.WAITING_OPPONENT, - DisputeStatus.OPPONENT_JOINED, - DisputeStatus.BOTH_SUBMITTED, - DisputeStatus.JUDGING, - ], - }, - } - : completed - ? { status: { in: [DisputeStatus.JUDGED, DisputeStatus.CLOSED] } } - : {}), + ...(date ? { + createdAt: { + gte: new Date(date), + lt: new Date(new Date(date).setDate(new Date(date).getDate() + 1)), + }, + } : {}), + // active=true 일 때 진행중 상태만 필터링 (status 파라미터가 없을 때만 적용) + // draft(진술 전), judged(판결 완료), closed/expired/deleted 제외 + ...(active && !rawStatus ? { + status: { + in: [ + DisputeStatus.WAITING_OPPONENT, + DisputeStatus.OPPONENT_JOINED, + DisputeStatus.BOTH_SUBMITTED, + DisputeStatus.JUDGING, + ], + }, + } : {}), + ...(rawStatus ? { status: rawStatus.toUpperCase() as DisputeStatus } : {}), } try { diff --git a/src/components/calendar/CalendarView.module.scss b/src/components/calendar/CalendarView.module.scss index 0985f5f..e08d22f 100644 --- a/src/components/calendar/CalendarView.module.scss +++ b/src/components/calendar/CalendarView.module.scss @@ -6,8 +6,13 @@ .emotionIcon { position: absolute; - bottom: 20px; + bottom: 4px; left: 50%; - transform: translateX(-45%); + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; pointer-events: none; } + diff --git a/src/components/calendar/CalendarView.tsx b/src/components/calendar/CalendarView.tsx index f761cb3..d1a115e 100644 --- a/src/components/calendar/CalendarView.tsx +++ b/src/components/calendar/CalendarView.tsx @@ -7,19 +7,31 @@ import { PickerDay } from '@mui/x-date-pickers'; import dayjs, { Dayjs } from 'dayjs'; import 'dayjs/locale/ko'; import EmotionIcon from '@/components/calendar/EmotionIcon'; +import CategoryIcon from '@/components/ui/CategoryIcon'; import styles from '@/components/calendar/CalendarView.module.scss'; -import type { DiaryDaySummary, CalendarViewProps } from '@/types/diary'; +import type { CalendarViewProps } from '@/types/diary'; +import type { CalendarRecordItem } from '@/types/calendar'; +import type { CategoryGroup } from '@/types/common'; + +// DB 영문 emotion_type → EmotionIcon이 기대하는 한글 Feel 매핑 +const EMOTION_TYPE_TO_FEEL: Record = { + happy: '기쁨', + sad: '슬픔', + neutral: '보통', + annoyed: '짜증', + angry: '화남', +}; type CustomDayProps = React.ComponentProps & { - diaryData?: Record; + recordMap?: Map; }; -function CustomDay({ diaryData, ...pickerDayProps }: CustomDayProps) { +function CustomDay({ recordMap, ...pickerDayProps }: CustomDayProps) { const { day } = pickerDayProps; const dateKey = (day as Dayjs).format('YYYY-MM-DD'); - const summary = diaryData?.[dateKey]; - const isSunday = (day as Dayjs).day() === 0; + const record = recordMap?.get(dateKey); const isOutsideMonth = pickerDayProps.outsideCurrentMonth; + const isSunday = (day as Dayjs).day() === 0; return (
@@ -32,41 +44,47 @@ function CustomDay({ diaryData, ...pickerDayProps }: CustomDayProps) { display: 'flex', alignItems: 'flex-start', paddingTop: '8px', - '&:hover': { - backgroundColor: 'var(--bg-surface-soft)', - }, + '&:hover': { backgroundColor: 'var(--bg-surface-soft)' }, '&.Mui-selected': { backgroundColor: 'var(--bg-surface-soft)', color: 'var(--text-primary)', }, - ...(isSunday && { - color: 'red', - }), + ...(isSunday && { color: 'red' }), }} /> - {summary && !isOutsideMonth && ( + {/* 이 날짜에 기록이 있고 이전 달/다음 달 날짜가 아닐 때만 아이콘 표시 */} + {record && !isOutsideMonth && (
- + {/* 감정일기가 있으면 감정 이모티콘 + 개수 */} + {record.diary && ( + + )} + {/* 종료된 사건이 있으면 카테고리 아이콘 + 개수 */} + {record.dispute && ( + + )}
)}
); } -const DUMMY_DIARY_DATA: Record = { - '2026-06-03': { feel: '기쁨', count: 1 }, - '2026-06-07': { feel: '슬픔', count: 2 }, - '2026-06-11': { feel: '보통', count: 1 }, - '2026-06-15': { feel: '짜증', count: 3 }, - '2026-06-20': { feel: '화남', count: 1 }, -}; - export default function CalendarView({ onDateChange, selectedDate, - diaryData = DUMMY_DIARY_DATA, + calendarData = [], }: CalendarViewProps) { + // 배열을 Map으로 변환 → CustomDay에서 O(1) 날짜 조회 + const recordMap = new Map(calendarData.map((r) => [r.date, r])); + return (
@@ -75,7 +93,7 @@ export default function CalendarView({ views={['year', 'month', 'day']} onChange={(date) => date && onDateChange(date.format('YYYY-MM-DD'))} dayOfWeekFormatter={(day) => ['S', 'M', 'T', 'W', 'T', 'F', 'S'][day.day()]} - slots={{ day: (props) => }} + slots={{ day: (props) => }} slotProps={{ calendarHeader: { format: 'YYYY년 M월' }, }} @@ -86,7 +104,6 @@ export default function CalendarView({ fontFamily: 'var(--font-pretendard), Pretendard, -apple-system, sans-serif', '& *': { fontFamily: 'inherit' }, - // 월 전환 시 높이 고정 — 없으면 레이아웃 밀림 발생 '& .MuiDayCalendar-slideTransition': { minHeight: 480, overflow: 'visible', diff --git a/src/components/calendar/RecordList.module.scss b/src/components/calendar/RecordList.module.scss index ae4331f..179c099 100644 --- a/src/components/calendar/RecordList.module.scss +++ b/src/components/calendar/RecordList.module.scss @@ -1,4 +1,6 @@ @use '../../styles/abstracts/functions' as fn; +@use '../../styles/abstracts/variables' as v; +@use '../../styles/abstracts/mixins' as m; .list { list-style: none; @@ -16,3 +18,49 @@ align-items: center; justify-content: center; } + +.cardWrapper { + display: flex; + flex-direction: column; + box-shadow: inset 0 0 0 2px v.$color-black-700; + border-radius: fn.r(12); + background-color: var(--bg-surface); + overflow: hidden; + text-decoration: none; + color: inherit; + + &:active { + opacity: 0.8; + } + + > article { + border: none; + border-radius: 0; + background-color: transparent; + padding-bottom: 0; + + time { display: none; } + + > p { margin-bottom: fn.r(8); } + } +} + +.cardFooter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 fn.r(16) fn.r(16); +} + +.cardFooterLeft { + display: flex; + align-items: center; + gap: fn.r(8); +} + + +.cardDate { + @include m.text-label-s; + + color: v.$color-black-400; +} diff --git a/src/components/calendar/RecordList.tsx b/src/components/calendar/RecordList.tsx index dbef70d..54e8e83 100644 --- a/src/components/calendar/RecordList.tsx +++ b/src/components/calendar/RecordList.tsx @@ -1,56 +1,51 @@ 'use client'; +import Link from 'next/link'; import CaseCard from '@/components/ui/CaseCard'; -import type { CategoryGroup } from '@/types/common'; +import Avatar from '@/components/ui/Avatar'; +import StatusBadge from '@/components/ui/StatusBadge'; +import { useDisputesByDate } from '@/domains/dispute/dispute.hooks'; +import type { DisputeStatus } from '@/components/ui/StatusBadge'; import styles from '@/components/calendar/RecordList.module.scss'; type Props = { selectedDate: string; }; -interface DummyItem { - id: number; - title: string; - preview: string; - date: string; - categoryGroup: CategoryGroup; +function formatDate(dateStr: string): string { + return dateStr.slice(2, 10); } -const DUMMY_ITEMS: DummyItem[] = [ - { - id: 1, - title: '남자친구와 데이트 비용 갈등', - preview: '매번 제가 더 많이 내는 것 같아서 억울한 마음이 들었어요.', - date: '2026.06.18', - categoryGroup: 'romance', - }, - { - id: 2, - title: '팀장님 업무 지시 방식 문제', - preview: '갑작스러운 야근 요청이 너무 잦아서 힘들었어요.', - date: '2026.06.15', - categoryGroup: 'work', - }, -]; - export default function RecordList({ selectedDate }: Props) { - // TODO: selectedDate 기반 API 연동 시 DUMMY_ITEMS 대체 - const items = DUMMY_ITEMS; + const { data, isLoading } = useDisputesByDate(selectedDate, 'closed'); + + if (isLoading) return
불러오는 중...
; + + const disputes = data?.disputes ?? []; - if (items.length === 0) { + if (disputes.length === 0) { return
등록한 기록이 없어요
; } return (
    - {items.map((item) => ( -
  • - + {disputes.map((dispute) => ( +
  • + + +
    +
    + + +
    + +
    +
  • ))}
diff --git a/src/components/diary/EmotionDiaryList.tsx b/src/components/diary/EmotionDiaryList.tsx index a33ac31..132fbfc 100644 --- a/src/components/diary/EmotionDiaryList.tsx +++ b/src/components/diary/EmotionDiaryList.tsx @@ -1,42 +1,25 @@ 'use client'; import DiaryCard from '@/components/diary/DiaryCard'; -import type { DiaryItem } from '@/types/diary'; +import { useDiariesByDate } from '@/domains/diary/diary.hooks'; import styles from '@/components/diary/EmotionDiaryList.module.scss'; -const DUMMY_ITEMS: DiaryItem[] = [ - { - id: '1', - title: '비가온다', - content: '우르르쾅쾅 마음이 무겁다', - date: '26-06-14', - emotionType: 'sad', - }, - { - id: '2', - title: '오늘은 기분이 좋아', - content: '오랜만에 친구를 만났다. 별거 아닌 얘기를 나눴는데 기분이 한결 가벼워졌다.', - date: '26-06-14', - emotionType: 'happy', - }, -]; - type Props = { selectedDate: string; }; export default function EmotionDiaryList({ selectedDate }: Props) { - if (DUMMY_ITEMS.length === 0) { - return ( -
- 등록한 일기가 없어요 -
- ); + const { data: items = [], isLoading } = useDiariesByDate(selectedDate); + + if (isLoading) return
불러오는 중...
; + + if (items.length === 0) { + return
등록한 일기가 없어요
; } return (
    - {DUMMY_ITEMS.map((item) => ( + {items.map((item) => (
  • diff --git a/src/components/ui/CategoryIcon.module.scss b/src/components/ui/CategoryIcon.module.scss new file mode 100644 index 0000000..8d2a51b --- /dev/null +++ b/src/components/ui/CategoryIcon.module.scss @@ -0,0 +1,22 @@ +.wrapper { + position: relative; + width: 24px; + height: 24px; + flex-shrink: 0; +} + +.badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 14px; + height: 14px; + padding: 0 3px; + border-radius: 50%; + background-color: var(--icon-primary); + color: #fff; + font-size: 10px; + font-weight: 700; + line-height: 14px; + text-align: center; +} diff --git a/src/components/ui/CategoryIcon.tsx b/src/components/ui/CategoryIcon.tsx index ecb3daf..861a2bb 100644 --- a/src/components/ui/CategoryIcon.tsx +++ b/src/components/ui/CategoryIcon.tsx @@ -1,4 +1,5 @@ import type { ElementType } from 'react'; +import styles from './CategoryIcon.module.scss'; import FavoriteIcon from '@mui/icons-material/Favorite'; import BusinessCenterIcon from '@mui/icons-material/BusinessCenter'; import Diversity3Icon from '@mui/icons-material/Diversity3'; @@ -28,18 +29,24 @@ const CATEGORY_COLOR_MAP: Record = { interface CategoryIconProps { category: CategoryGroup; + count?: number; } -export default function CategoryIcon({ category }: CategoryIconProps) { +export default function CategoryIcon({ category, count }: CategoryIconProps) { const Icon = CATEGORY_ICON_MAP[category]; return ( - +
    + + {count !== undefined && count > 1 && ( + {count} + )} +
    ); } diff --git a/src/domains/calendar/.gitkeep b/src/domains/calendar/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/domains/calendar/calendar.api.ts b/src/domains/calendar/calendar.api.ts new file mode 100644 index 0000000..80e93bf --- /dev/null +++ b/src/domains/calendar/calendar.api.ts @@ -0,0 +1,9 @@ +import type { ApiResponse } from '@/types/common' +import type { CalendarMonthResponse } from '@/types/calendar' + +export async function fetchCalendarRecords(year: number, month: number): Promise { + const res = await fetch(`/api/calendar?year=${year}&month=${month}`) + const json: ApiResponse = await res.json() + if (!json.success || !json.data) throw new Error(json.error?.message ?? '캘린더 기록 조회 실패') + return json.data +} diff --git a/src/domains/calendar/calendar.hooks.ts b/src/domains/calendar/calendar.hooks.ts new file mode 100644 index 0000000..6d11702 --- /dev/null +++ b/src/domains/calendar/calendar.hooks.ts @@ -0,0 +1,17 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' +import { fetchCalendarRecords } from './calendar.api' + +export const calendarKeys = { + month: (year: number, month: number) => ['calendar', year, month] as const, +} + +export function useCalendarRecords(year: number, month: number) { + return useQuery({ + queryKey: calendarKeys.month(year, month), + queryFn: () => fetchCalendarRecords(year, month), + enabled: !!year && !!month, + staleTime: 1000 * 60 * 5, + }) +} diff --git a/src/domains/diary/diary.api.ts b/src/domains/diary/diary.api.ts new file mode 100644 index 0000000..8017d12 --- /dev/null +++ b/src/domains/diary/diary.api.ts @@ -0,0 +1,9 @@ +import type { ApiResponse } from '@/types/common' +import type { DiaryItem } from '@/types/diary' + +export async function fetchDiariesByDate(date: string): Promise { + const res = await fetch(`/api/diary?date=${date}`) + const json: ApiResponse<{ items: DiaryItem[] }> = await res.json() + if (!json.success || !json.data) throw new Error(json.error?.message ?? '감정일기 조회 실패') + return json.data.items +} diff --git a/src/domains/diary/diary.hooks.ts b/src/domains/diary/diary.hooks.ts new file mode 100644 index 0000000..e1b2fef --- /dev/null +++ b/src/domains/diary/diary.hooks.ts @@ -0,0 +1,17 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' +import { fetchDiariesByDate } from './diary.api' + +export const diaryKeys = { + byDate: (date: string) => ['diary', 'byDate', date] as const, +} + +export function useDiariesByDate(date: string) { + return useQuery({ + queryKey: diaryKeys.byDate(date), + queryFn: () => fetchDiariesByDate(date), + enabled: !!date, + staleTime: 1000 * 60 * 5, + }) +} diff --git a/src/domains/dispute/dispute.api.ts b/src/domains/dispute/dispute.api.ts index de28643..3ee4829 100644 --- a/src/domains/dispute/dispute.api.ts +++ b/src/domains/dispute/dispute.api.ts @@ -2,11 +2,12 @@ import type { ApiResponse, CategoryGroup } from '@/types/common' import type { DisputeDto, DisputeListResponse, - DisputeStatementDto, + DisputeStatus, SaveStatementResponse, StatementSubmitResponse, } from '@/types/dispute' + async function parseJson(res: Response, fallbackMessage: string): Promise> { const text = await res.text() try { @@ -15,7 +16,6 @@ async function parseJson(res: Response, fallbackMessage: string): Promise { const params = new URLSearchParams({ completed: 'true', limit: '50' }) if (categoryGroup) params.set('categoryGroup', categoryGroup) @@ -25,6 +25,15 @@ export async function fetchCompletedCases(categoryGroup?: CategoryGroup): Promis return json.data.disputes } +export async function fetchDisputesByDate(date: string, status?: DisputeStatus): Promise { + const params = new URLSearchParams({ date, limit: '50' }) + if (status) params.set('status', status) + const res = await fetch(`/api/disputes?${params.toString()}`) + const json = await parseJson(res, '사건 목록 조회 실패') + if (!json.success || !json.data) throw new Error(json.error?.message ?? '사건 목록 조회 실패') + return json.data +} + export async function fetchDispute(disputeId: string): Promise { const res = await fetch(`/api/disputes/${disputeId}`) const json = await parseJson(res, '사건 조회 실패') diff --git a/src/domains/dispute/dispute.hooks.ts b/src/domains/dispute/dispute.hooks.ts index 3f00ce9..5b2f86a 100644 --- a/src/domains/dispute/dispute.hooks.ts +++ b/src/domains/dispute/dispute.hooks.ts @@ -1,21 +1,40 @@ -'use client' +'use client'; -import { useQuery, useMutation, useQueryClient, UseQueryOptions } from '@tanstack/react-query' -import { fetchDispute, fetchCompletedCases, saveStatement, submitStatement, requestJudgment, closeDispute } from './dispute.api' -import type { CategoryGroup } from '@/types/common' -import type { DisputeDto } from '@/types/dispute' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import type { CategoryGroup } from '@/types/common'; +import type { DisputeDto, DisputeStatus } from '@/types/dispute'; +import { + fetchCompletedCases, + fetchDisputesByDate, + fetchDispute, + saveStatement, + submitStatement, + requestJudgment, + closeDispute, +} from './dispute.api'; export const disputeKeys = { detail: (id: string) => ['dispute', id] as const, - completedList: (categoryGroup?: CategoryGroup) => ['disputes', 'completed', categoryGroup ?? 'all'] as const, -} + completedList: (categoryGroup?: CategoryGroup) => + ['disputes', 'completed', categoryGroup ?? 'all'] as const, + byDate: (date: string, status?: DisputeStatus) => ['disputes', 'byDate', date, status] as const, +}; export function useCompletedCases(categoryGroup?: CategoryGroup) { return useQuery({ queryKey: disputeKeys.completedList(categoryGroup), queryFn: () => fetchCompletedCases(categoryGroup), staleTime: 1000 * 60, - }) + }); +} + +export function useDisputesByDate(date: string, status?: DisputeStatus) { + return useQuery({ + queryKey: disputeKeys.byDate(date, status), + queryFn: () => fetchDisputesByDate(date, status), + enabled: !!date, + }); } export function useDispute( @@ -27,45 +46,45 @@ export function useDispute( queryFn: () => fetchDispute(disputeId), enabled: !!disputeId, refetchInterval: options?.refetchInterval, - }) + }); } export function useSaveStatement(disputeId: string) { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: (content: string) => saveStatement(disputeId, content), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }) + queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, - }) + }); } export function useSubmitStatement(disputeId: string) { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: () => submitStatement(disputeId), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }) + queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, - }) + }); } export function useRequestJudgment(disputeId: string) { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: () => requestJudgment(disputeId), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }) + queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, - }) + }); } export function useCloseDispute(disputeId: string) { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: () => closeDispute(disputeId), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }) + queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, - }) + }); } diff --git a/src/types/calendar.ts b/src/types/calendar.ts new file mode 100644 index 0000000..39a1679 --- /dev/null +++ b/src/types/calendar.ts @@ -0,0 +1,21 @@ +export interface CalendarDiarySummary { + count: number + emotion: string +} + +export interface CalendarDisputeSummary { + count: number + category: string +} + +export interface CalendarRecordItem { + date: string + diary: CalendarDiarySummary | null + dispute: CalendarDisputeSummary | null +} + +export interface CalendarMonthResponse { + year: number + month: number + records: CalendarRecordItem[] +} diff --git a/src/types/diary.ts b/src/types/diary.ts index 33ca656..844d720 100644 --- a/src/types/diary.ts +++ b/src/types/diary.ts @@ -20,7 +20,7 @@ export type DiaryTab = 'emotion' | 'record' export interface CalendarViewProps { onDateChange: (date: string) => void selectedDate?: string - diaryData?: Record + calendarData?: import('@/types/calendar').CalendarRecordItem[] } /** EmotionIcon 컴포넌트 Props */ From ba7c079b3fc826788100f7f3581c7f61dbf96211 Mon Sep 17 00:00:00 2001 From: LSG Date: Mon, 22 Jun 2026 11:09:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20diary=20,=20calendar=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=9D=91=EB=8B=B5=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/calendar/route.ts | 94 ++++++++++++----------- src/app/api/diary/route.ts | 66 ++++++++++------ src/components/calendar/RecordList.tsx | 3 +- src/components/diary/EmotionDiaryList.tsx | 3 +- 4 files changed, 95 insertions(+), 71 deletions(-) diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts index 74cca4c..d3136ee 100644 --- a/src/app/api/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -1,42 +1,44 @@ -import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' -import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' -import type { ApiResponse } from '@/types/common' -import type { CalendarMonthResponse, CalendarRecordItem } from '@/types/calendar' +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/db'; +import { getSessionUserId } from '@/lib/auth/session'; +import type { ApiResponse } from '@/types/common'; +import type { CalendarMonthResponse, CalendarRecordItem } from '@/types/calendar'; // GET /api/calendar?year=2026&month=6 // 해당 월의 날짜별 감정일기/사건 요약 반환 (달력 마킹용) export async function GET(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) - if (!userId) { - return NextResponse.json( - { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, - { status: 401 }, - ) - } + try { + const session = await getServerSession(authOptions); + const userId = getSessionUserId(session); + if (!userId) { + return NextResponse.json( + { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, + { status: 401 }, + ); + } - const { searchParams } = new URL(request.url) - const year = parseInt(searchParams.get('year') ?? '', 10) - const month = parseInt(searchParams.get('month') ?? '', 10) + const { searchParams } = new URL(request.url); + const year = parseInt(searchParams.get('year') ?? '', 10); + const month = parseInt(searchParams.get('month') ?? '', 10); - if (isNaN(year) || isNaN(month) || month < 1 || month > 12) { - return NextResponse.json( - { success: false, error: { code: 'VALIDATION_ERROR', message: 'year, month가 올바르지 않습니다.' } }, - { status: 400 }, - ) - } + if (isNaN(year) || isNaN(month) || month < 1 || month > 12) { + return NextResponse.json( + { + success: false, + error: { code: 'VALIDATION_ERROR', message: 'year, month가 올바르지 않습니다.' }, + }, + { status: 400 }, + ); + } - // 해당 월의 시작일(1일 00:00)과 종료일(다음달 1일 00:00) 계산 - const startDate = new Date(year, month - 1, 1) - const endDate = new Date(year, month, 1) + // 해당 월의 시작일(1일 00:00)과 종료일(다음달 1일 00:00) 계산 + const startDate = new Date(year, month - 1, 1); + const endDate = new Date(year, month, 1); - try { // 두 쿼리를 병렬로 실행해 응답 시간 단축 const [diaries, disputes] = await Promise.all([ - // 감정일기: 해당 월 전체 조회 (diaryDate + emotionType만 선택) // 최신순 정렬 → 날짜별 첫 번째 항목이 그 날의 최신 감정 prisma.emotionDiary.findMany({ @@ -61,33 +63,33 @@ export async function GET(request: NextRequest) { select: { createdAt: true, categoryGroup: true }, orderBy: { createdAt: 'desc' }, }), - ]) + ]); // 감정일기 날짜별 그루핑 // Map key: "YYYY-MM-DD", value: { count, emotion } // 이미 최신순 정렬이므로 날짜 첫 등장 시 저장한 emotion이 그 날의 최신 emotion - const diaryMap = new Map() + const diaryMap = new Map(); for (const d of diaries) { - const date = d.diaryDate.toISOString().slice(0, 10) + const date = d.diaryDate.toISOString().slice(0, 10); if (!diaryMap.has(date)) { - diaryMap.set(date, { count: 0, emotion: d.emotionType ?? 'neutral' }) + diaryMap.set(date, { count: 0, emotion: d.emotionType ?? 'neutral' }); } - diaryMap.get(date)!.count++ + diaryMap.get(date)!.count++; } // 사건 날짜별 그루핑 // createdAt 기준으로 날짜 추출 → 동일 방식으로 최신 category 저장 - const disputeMap = new Map() + const disputeMap = new Map(); for (const d of disputes) { - const date = d.createdAt.toISOString().slice(0, 10) + const date = d.createdAt.toISOString().slice(0, 10); if (!disputeMap.has(date)) { - disputeMap.set(date, { count: 0, category: d.categoryGroup.toLowerCase() }) + disputeMap.set(date, { count: 0, category: d.categoryGroup.toLowerCase() }); } - disputeMap.get(date)!.count++ + disputeMap.get(date)!.count++; } // 두 Map의 날짜를 합쳐서 중복 없이 전체 날짜 목록 생성 - const allDates = new Set([...diaryMap.keys(), ...disputeMap.keys()]) + const allDates = new Set([...diaryMap.keys(), ...disputeMap.keys()]); // 각 날짜별로 diary/dispute 요약 조합 // 둘 중 하나만 있는 날짜는 없는 쪽을 null로 처리 @@ -95,16 +97,20 @@ export async function GET(request: NextRequest) { date, diary: diaryMap.get(date) ?? null, dispute: disputeMap.get(date) ?? null, - })) + })); return NextResponse.json>({ success: true, data: { year, month, records }, - }) - } catch { + }); + } catch (error) { + console.error('[GET /api/calendar] failed', error); return NextResponse.json( - { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { + success: false, + error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' }, + }, { status: 500 }, - ) + ); } } diff --git a/src/app/api/diary/route.ts b/src/app/api/diary/route.ts index bee85b8..1c8c208 100644 --- a/src/app/api/diary/route.ts +++ b/src/app/api/diary/route.ts @@ -1,36 +1,48 @@ -import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' -import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' -import type { ApiResponse } from '@/types/common' -import type { DiaryItem } from '@/types/diary' +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/db'; +import { getSessionUserId } from '@/lib/auth/session'; +import type { ApiResponse } from '@/types/common'; +import type { DiaryItem } from '@/types/diary'; // GET /api/diary?date=2026-06-16 // 특정 날짜의 감정일기 목록 조회. 최신순 정렬. 본인 데이터만 반환 export async function GET(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const session = await getServerSession(authOptions); + const userId = getSessionUserId(session); if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, { status: 401 }, - ) + ); } - const { searchParams } = new URL(request.url) - const date = searchParams.get('date') + const { searchParams } = new URL(request.url); + const date = searchParams.get('date'); if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { return NextResponse.json( - { success: false, error: { code: 'VALIDATION_ERROR', message: 'date 파라미터가 필요합니다. (YYYY-MM-DD)' } }, + { + success: false, + error: { code: 'VALIDATION_ERROR', message: 'date 파라미터가 필요합니다. (YYYY-MM-DD)' }, + }, { status: 400 }, - ) + ); } - - const targetDate = new Date(date) - const nextDate = new Date(targetDate) - nextDate.setDate(nextDate.getDate() + 1) + const targetDate = new Date(`${date}T00:00:00.000Z`); + if (Number.isNaN(targetDate.getTime()) || targetDate.toISOString().slice(0, 10) !== date) { + return NextResponse.json( + { + success: false, + error: { code: 'VALIDATION_ERROR', message: '유효한 date가 아닙니다. (YYYY-MM-DD)' }, + }, + { status: 400 }, + ); + } + const nextDate = new Date(targetDate); + // nextDate.setDate(nextDate.getDate() + 1); + nextDate.setUTCDate(nextDate.getUTCDate() + 1) try { const diaries = await prisma.emotionDiary.findMany({ @@ -47,7 +59,7 @@ export async function GET(request: NextRequest) { emotionType: true, }, orderBy: { createdAt: 'desc' }, - }) + }); const items: DiaryItem[] = diaries.map((d) => ({ id: d.id, @@ -55,16 +67,20 @@ export async function GET(request: NextRequest) { content: d.content, date: d.diaryDate.toISOString().slice(0, 10), emotionType: (d.emotionType ?? 'neutral') as DiaryItem['emotionType'], - })) + })); return NextResponse.json>({ success: true, data: { items }, - }) - } catch { + }); + } catch (error) { + console.log(error); return NextResponse.json( - { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { + success: false, + error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' }, + }, { status: 500 }, - ) + ); } -} \ No newline at end of file +} diff --git a/src/components/calendar/RecordList.tsx b/src/components/calendar/RecordList.tsx index 54e8e83..44eca5b 100644 --- a/src/components/calendar/RecordList.tsx +++ b/src/components/calendar/RecordList.tsx @@ -17,9 +17,10 @@ function formatDate(dateStr: string): string { } export default function RecordList({ selectedDate }: Props) { - const { data, isLoading } = useDisputesByDate(selectedDate, 'closed'); + const { data, isLoading, isError } = useDisputesByDate(selectedDate, 'closed'); if (isLoading) return
    불러오는 중...
    ; + if (isError) return
    기록을 불러오지 못했어요
    ; const disputes = data?.disputes ?? []; diff --git a/src/components/diary/EmotionDiaryList.tsx b/src/components/diary/EmotionDiaryList.tsx index 132fbfc..4a7b09d 100644 --- a/src/components/diary/EmotionDiaryList.tsx +++ b/src/components/diary/EmotionDiaryList.tsx @@ -9,9 +9,10 @@ type Props = { }; export default function EmotionDiaryList({ selectedDate }: Props) { - const { data: items = [], isLoading } = useDiariesByDate(selectedDate); + const { data: items = [], isLoading, isError } = useDiariesByDate(selectedDate); if (isLoading) return
    불러오는 중...
    ; + if (isError) return
    일기를 불러오지 못했어요
    ; if (items.length === 0) { return
    등록한 일기가 없어요
    ; From c8776976c639ef49b3b98e53f0ded52a610fd5b1 Mon Sep 17 00:00:00 2001 From: LSG Date: Mon, 22 Jun 2026 19:10:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EA=B0=90=EC=A0=95=EC=9D=BC?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95=20,=20=EC=A1=B0=ED=9A=8C=20api?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../diary/[id]/DiaryActions.module.scss | 22 +++ src/app/(page)/diary/[id]/DiaryActions.tsx | 67 +++++++++ .../(page)/diary/[id]/DiaryDetailHeader.tsx | 9 ++ src/app/(page)/diary/[id]/edit/page.tsx | 22 +++ src/app/api/diary/[diaryId]/route.ts | 134 ++++++++++++++++++ src/app/api/diary/route.ts | 6 +- src/app/api/disputes/route.ts | 4 - src/domains/diary/diary.service.ts | 101 +++++++++++++ src/domains/dispute/dispute.hooks.ts | 27 +--- 9 files changed, 359 insertions(+), 33 deletions(-) create mode 100644 src/app/(page)/diary/[id]/DiaryActions.module.scss create mode 100644 src/app/(page)/diary/[id]/DiaryActions.tsx create mode 100644 src/app/(page)/diary/[id]/DiaryDetailHeader.tsx create mode 100644 src/app/(page)/diary/[id]/edit/page.tsx create mode 100644 src/app/api/diary/[diaryId]/route.ts create mode 100644 src/domains/diary/diary.service.ts diff --git a/src/app/(page)/diary/[id]/DiaryActions.module.scss b/src/app/(page)/diary/[id]/DiaryActions.module.scss new file mode 100644 index 0000000..3f5d177 --- /dev/null +++ b/src/app/(page)/diary/[id]/DiaryActions.module.scss @@ -0,0 +1,22 @@ +@use '@/styles/abstracts/variables' as v; +@use '@/styles/abstracts/functions' as fn; +@use '@/styles/abstracts/mixins' as typo; + +.modalContent { + display: flex; + flex-direction: column; + gap: fn.r(20); +} + +.modalTitle { + @include typo.text-title-s +} + +.modalButtons { + display: flex; + gap: fn.r(8); + + > * { + flex: 1; + } +} diff --git a/src/app/(page)/diary/[id]/DiaryActions.tsx b/src/app/(page)/diary/[id]/DiaryActions.tsx new file mode 100644 index 0000000..834570f --- /dev/null +++ b/src/app/(page)/diary/[id]/DiaryActions.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import ActionPrompt from '@/components/ui/ActionPrompt'; +import Button from '@/components/ui/Button'; +import Modal from '@/components/ui/Modal'; +import { useDeleteDiary } from '@/domains/diary/diary.hooks'; +import styles from './DiaryActions.module.scss'; + +interface DiaryActionsProps { + diaryId: string; + diaryDate: string; + className?: string; +} + +export default function DiaryActions({ diaryId, diaryDate, className }: DiaryActionsProps) { + const router = useRouter(); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const deleteMutation = useDeleteDiary(diaryDate); + + const handleEdit = () => { + router.push(`/diary/${diaryId}/edit`); + }; + + const handleDelete = () => { + setShowDeleteModal(true); + }; + + const handleConfirmDelete = () => { + deleteMutation.mutate(diaryId, { + onError: () => setShowDeleteModal(false), + }); + }; + + return ( + <> + + + !deleteMutation.isPending && setShowDeleteModal(false)}> +
    +

    일기를 삭제하시겠습니까?

    +
    + + +
    +
    +
    + + ); +} diff --git a/src/app/(page)/diary/[id]/DiaryDetailHeader.tsx b/src/app/(page)/diary/[id]/DiaryDetailHeader.tsx new file mode 100644 index 0000000..9794bfe --- /dev/null +++ b/src/app/(page)/diary/[id]/DiaryDetailHeader.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import Header from '@/components/layout/Header'; + +export default function DiaryDetailHeader() { + const router = useRouter(); + return
    router.push('/calendar')} />; +} diff --git a/src/app/(page)/diary/[id]/edit/page.tsx b/src/app/(page)/diary/[id]/edit/page.tsx new file mode 100644 index 0000000..514e035 --- /dev/null +++ b/src/app/(page)/diary/[id]/edit/page.tsx @@ -0,0 +1,22 @@ +import { notFound, redirect } from 'next/navigation'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { getSessionUserId } from '@/lib/auth/session'; +import { getDiaryById } from '@/domains/diary/diary.service'; +import DiaryCreate from '@/app/(page)/diary/create/page'; + +export default async function DiaryEditPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const session = await getServerSession(authOptions); + const userId = getSessionUserId(session); + if (!userId) redirect('/login'); + + const { id } = await params; + const diary = await getDiaryById(id, userId); + if (!diary) notFound(); + + return ; +} diff --git a/src/app/api/diary/[diaryId]/route.ts b/src/app/api/diary/[diaryId]/route.ts new file mode 100644 index 0000000..8073cfe --- /dev/null +++ b/src/app/api/diary/[diaryId]/route.ts @@ -0,0 +1,134 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/db'; +import { getSessionUserId } from '@/lib/auth/session'; +import { deleteDiaryById, updateDiaryById } from '@/domains/diary/diary.service'; +import type { ApiResponse } from '@/types/common'; +import type { DiaryDetail } from '@/types/diary'; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ diaryId: string }> }, +) { + const session = await getServerSession(authOptions); + const userId = getSessionUserId(session); + if (!userId) { + return NextResponse.json( + { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, + { status: 401 }, + ); + } + + const { diaryId } = await params; + + try { + const diary = await prisma.emotionDiary.findUnique({ + where: { id: diaryId, deletedAt: null }, + select: { + id: true, + title: true, + content: true, + diaryDate: true, + emotionType: true, + createdAt: true, + updatedAt: true, + userId: true, + }, + }); + + if (!diary) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '일기를 찾을 수 없습니다.' } }, + { status: 404 }, + ); + } + + if (diary.userId !== userId) { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '접근 권한이 없습니다.' } }, + { status: 403 }, + ); + } + + return NextResponse.json>({ + success: true, + data: { + id: diary.id, + title: diary.title ?? '', + diaryDate: diary.diaryDate.toISOString().slice(0, 10), + emotionType: diary.emotionType ?? null, + content: diary.content, + createdAt: diary.createdAt.toISOString(), + updatedAt: diary.updatedAt.toISOString(), + }, + }); + } catch (error) { + console.error(error); + return NextResponse.json( + { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { status: 500 }, + ); + } +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ diaryId: string }> }, +) { + const session = await getServerSession(authOptions); + const userId = getSessionUserId(session); + if (!userId) { + return NextResponse.json( + { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, + { status: 401 }, + ); + } + + const { diaryId } = await params; + const body = await request.json(); + const { title, content, emotionType } = body; + + if (!content) { + return NextResponse.json( + { success: false, error: { code: 'VALIDATION_ERROR', message: '내용은 필수입니다.' } }, + { status: 400 }, + ); + } + + const updated = await updateDiaryById(diaryId, userId, { title: title ?? '', content, emotionType: emotionType ?? 'neutral' }); + if (!updated) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '일기를 찾을 수 없습니다.' } }, + { status: 404 }, + ); + } + + return NextResponse.json({ success: true }); +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: Promise<{ diaryId: string }> }, +) { + const session = await getServerSession(authOptions); + const userId = getSessionUserId(session); + if (!userId) { + return NextResponse.json( + { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, + { status: 401 }, + ); + } + + const { diaryId } = await params; + + const deleted = await deleteDiaryById(diaryId, userId); + if (!deleted) { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '일기를 찾을 수 없습니다.' } }, + { status: 404 }, + ); + } + + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/diary/route.ts b/src/app/api/diary/route.ts index 5161c09..03c192e 100644 --- a/src/app/api/diary/route.ts +++ b/src/app/api/diary/route.ts @@ -74,8 +74,7 @@ export async function GET(request: NextRequest) { ); } const nextDate = new Date(targetDate); - // nextDate.setDate(nextDate.getDate() + 1); - nextDate.setUTCDate(nextDate.getUTCDate() + 1) + nextDate.setUTCDate(nextDate.getUTCDate() + 1); try { const diaries = await prisma.emotionDiary.findMany({ @@ -106,8 +105,7 @@ export async function GET(request: NextRequest) { success: true, data: { items }, }); - } catch (error) { - console.log(error); + } catch { return NextResponse.json( { success: false, diff --git a/src/app/api/disputes/route.ts b/src/app/api/disputes/route.ts index 8baec5c..d6e51fd 100644 --- a/src/app/api/disputes/route.ts +++ b/src/app/api/disputes/route.ts @@ -78,15 +78,11 @@ export async function GET(request: NextRequest) { const rawCategory = searchParams.get('categoryGroup') const date = searchParams.get('date') const rawStatus = searchParams.get('status') - const date = searchParams.get('date') - const rawStatus = searchParams.get('status') // URL 쿼리 파라미터는 문자열로 전달되므로 "true" 문자열과 비교 const active = searchParams.get('active') === 'true' const page = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10)) const limit = Math.min(50, Math.max(1, parseInt(searchParams.get('limit') ?? '20', 10))) - - if (rawCategory !== null && !VALID_CATEGORY_GROUPS.includes(rawCategory as CategoryGroup)) { return NextResponse.json( { diff --git a/src/domains/diary/diary.service.ts b/src/domains/diary/diary.service.ts new file mode 100644 index 0000000..37ffe08 --- /dev/null +++ b/src/domains/diary/diary.service.ts @@ -0,0 +1,101 @@ +import { prisma } from '@/lib/db'; + +export async function createDiary( + userId: string, + data: { title: string; content: string; emotionType: string; diaryDate: string }, +): Promise { + try { + const diary = await prisma.emotionDiary.create({ + data: { + userId, + title: data.title, + content: data.content, + emotionType: data.emotionType, + diaryDate: new Date(`${data.diaryDate}T00:00:00.000Z`), + }, + }); + return diary.id; + } catch (error) { + console.error('[createDiary]', error); + return null; + } +} + +export async function updateDiaryById( + diaryId: string, + userId: string, + data: { title: string; content: string; emotionType: string }, +): Promise { + try { + const diary = await prisma.emotionDiary.findUnique({ + where: { id: diaryId, deletedAt: null }, + select: { userId: true }, + }); + + if (!diary || diary.userId !== userId) return false; + + await prisma.emotionDiary.update({ + where: { id: diaryId }, + data, + }); + + return true; + } catch (error) { + console.error('[updateDiaryById]', error); + return false; + } +} + +export async function deleteDiaryById(diaryId: string, userId: string): Promise { + try { + const diary = await prisma.emotionDiary.findUnique({ + where: { id: diaryId, deletedAt: null }, + select: { userId: true }, + }); + + if (!diary || diary.userId !== userId) return false; + + await prisma.emotionDiary.update({ + where: { id: diaryId }, + data: { deletedAt: new Date() }, + }); + + return true; + } catch (error) { + console.error('[deleteDiaryById]', error); + return false; + } +} + +export async function getDiaryById(diaryId: string, userId: string) { + try { + const diary = await prisma.emotionDiary.findUnique({ + where: { id: diaryId, deletedAt: null }, + select: { + id: true, + title: true, + content: true, + diaryDate: true, + emotionType: true, + createdAt: true, + updatedAt: true, + userId: true, + }, + }); + + if (!diary || diary.userId !== userId) return null; + + return { + id: diary.id, + title: diary.title ?? '', + content: diary.content, + diaryDate: diary.diaryDate.toISOString().slice(0, 10), + emotionType: diary.emotionType ?? null, + createdAt: diary.createdAt.toISOString(), + updatedAt: diary.updatedAt.toISOString(), + }; + } catch (error) { + console.error('[getDiaryById]', error); + return null; + } +} diff --git a/src/domains/dispute/dispute.hooks.ts b/src/domains/dispute/dispute.hooks.ts index cfb5f56..3a3f914 100644 --- a/src/domains/dispute/dispute.hooks.ts +++ b/src/domains/dispute/dispute.hooks.ts @@ -1,6 +1,6 @@ 'use client'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; import type { UseQueryOptions } from '@tanstack/react-query'; import type { CategoryGroup } from '@/types/common'; import type { DisputeDto, DisputeStatus } from '@/types/dispute'; @@ -20,30 +20,17 @@ export const disputeKeys = { ['disputes', 'completed', categoryGroup ?? 'all'] as const, byDate: (date: string, status?: DisputeStatus) => ['disputes', 'byDate', date, status] as const, }; - completedList: (categoryGroup?: CategoryGroup) => - ['disputes', 'completed', categoryGroup ?? 'all'] as const, - byDate: (date: string, status?: DisputeStatus) => ['disputes', 'byDate', date, status] as const, -}; export function useCompletedCases(categoryGroup?: CategoryGroup) { return useInfiniteQuery({ queryKey: disputeKeys.completedList(categoryGroup), queryFn: ({ pageParam }) => fetchCompletedCases(categoryGroup, pageParam), initialPageParam: 1, - getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.page + 1 : undefined, + getNextPageParam: (lastPage) => (lastPage.hasNext ? lastPage.page + 1 : undefined), staleTime: 1000 * 60, }); } -export function useDisputesByDate(date: string, status?: DisputeStatus) { - return useQuery({ - queryKey: disputeKeys.byDate(date, status), - queryFn: () => fetchDisputesByDate(date, status), - enabled: !!date, - }); - }); -} - export function useDisputesByDate(date: string, status?: DisputeStatus) { return useQuery({ queryKey: disputeKeys.byDate(date, status), @@ -63,23 +50,19 @@ export function useDispute( staleTime: 1000 * 30, refetchInterval: options?.refetchInterval, }); - }); } export function useSaveStatement(disputeId: string) { - const queryClient = useQueryClient(); const queryClient = useQueryClient(); return useMutation({ mutationFn: (content: string) => saveStatement(disputeId, content), onSuccess: () => { queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); - queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, }); } export function useSubmitStatement(disputeId: string) { - const queryClient = useQueryClient(); const queryClient = useQueryClient(); return useMutation({ mutationFn: () => submitStatement(disputeId), @@ -87,20 +70,16 @@ export function useSubmitStatement(disputeId: string) { queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, }); - }); } export function useRequestJudgment(disputeId: string) { - const queryClient = useQueryClient(); const queryClient = useQueryClient(); return useMutation({ mutationFn: () => requestJudgment(disputeId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); - queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, }); - }); } export function useCloseDispute(disputeId: string) { @@ -109,8 +88,6 @@ export function useCloseDispute(disputeId: string) { mutationFn: () => closeDispute(disputeId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); - queryClient.invalidateQueries({ queryKey: disputeKeys.detail(disputeId) }); }, }); - }); } From 5d0617b9451c5e78c2a9ec72fd6fc372833149e7 Mon Sep 17 00:00:00 2001 From: LSG Date: Tue, 23 Jun 2026 09:37:06 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor=20:=20diary=20route=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/diary/[diaryId]/route.ts | 70 ++++++++++++++++++++++------ src/app/api/diary/route.ts | 46 +++++++++++++++--- src/components/layout/Header.tsx | 17 ++++--- src/domains/diary/diary.service.ts | 61 +++++++++++------------- src/types/diary.ts | 2 +- 5 files changed, 134 insertions(+), 62 deletions(-) diff --git a/src/app/api/diary/[diaryId]/route.ts b/src/app/api/diary/[diaryId]/route.ts index 8073cfe..a497001 100644 --- a/src/app/api/diary/[diaryId]/route.ts +++ b/src/app/api/diary/[diaryId]/route.ts @@ -5,7 +5,7 @@ import { prisma } from '@/lib/db'; import { getSessionUserId } from '@/lib/auth/session'; import { deleteDiaryById, updateDiaryById } from '@/domains/diary/diary.service'; import type { ApiResponse } from '@/types/common'; -import type { DiaryDetail } from '@/types/diary'; +import type { DiaryDetail, EmotionType } from '@/types/diary'; export async function GET( _request: NextRequest, @@ -57,7 +57,7 @@ export async function GET( id: diary.id, title: diary.title ?? '', diaryDate: diary.diaryDate.toISOString().slice(0, 10), - emotionType: diary.emotionType ?? null, + emotionType: (diary.emotionType as EmotionType | null) ?? null, content: diary.content, createdAt: diary.createdAt.toISOString(), updatedAt: diary.updatedAt.toISOString(), @@ -86,7 +86,17 @@ export async function PATCH( } const { diaryId } = await params; - const body = await request.json(); + + let body: { title?: string; content?: string; emotionType?: string }; + try { + body = await request.json(); + } catch { + return NextResponse.json( + { success: false, error: { code: 'INVALID_JSON', message: '요청 본문이 올바른 JSON 형식이 아닙니다.' } }, + { status: 400 }, + ); + } + const { title, content, emotionType } = body; if (!content) { @@ -96,15 +106,30 @@ export async function PATCH( ); } - const updated = await updateDiaryById(diaryId, userId, { title: title ?? '', content, emotionType: emotionType ?? 'neutral' }); - if (!updated) { + try { + const result = await updateDiaryById(diaryId, userId, { title: title ?? '', content, emotionType: emotionType ?? 'neutral' }); + + if (result === 'not_found') { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '일기를 찾을 수 없습니다.' } }, + { status: 404 }, + ); + } + if (result === 'forbidden') { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '접근 권한이 없습니다.' } }, + { status: 403 }, + ); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('[PATCH /api/diary/:diaryId]', error); return NextResponse.json( - { success: false, error: { code: 'NOT_FOUND', message: '일기를 찾을 수 없습니다.' } }, - { status: 404 }, + { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { status: 500 }, ); } - - return NextResponse.json({ success: true }); } export async function DELETE( @@ -122,13 +147,28 @@ export async function DELETE( const { diaryId } = await params; - const deleted = await deleteDiaryById(diaryId, userId); - if (!deleted) { + try { + const result = await deleteDiaryById(diaryId, userId); + + if (result === 'not_found') { + return NextResponse.json( + { success: false, error: { code: 'NOT_FOUND', message: '일기를 찾을 수 없습니다.' } }, + { status: 404 }, + ); + } + if (result === 'forbidden') { + return NextResponse.json( + { success: false, error: { code: 'FORBIDDEN', message: '접근 권한이 없습니다.' } }, + { status: 403 }, + ); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('[DELETE /api/diary/:diaryId]', error); return NextResponse.json( - { success: false, error: { code: 'NOT_FOUND', message: '일기를 찾을 수 없습니다.' } }, - { status: 404 }, + { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { status: 500 }, ); } - - return NextResponse.json({ success: true }); } diff --git a/src/app/api/diary/route.ts b/src/app/api/diary/route.ts index 03c192e..a802cb5 100644 --- a/src/app/api/diary/route.ts +++ b/src/app/api/diary/route.ts @@ -18,7 +18,16 @@ export async function POST(request: NextRequest) { ); } - const body = await request.json(); + let body: { title?: string; content?: string; emotionType?: string; diaryDate?: string }; + try { + body = await request.json(); + } catch { + return NextResponse.json( + { success: false, error: { code: 'INVALID_JSON', message: '요청 본문이 올바른 JSON 형식이 아닙니다.' } }, + { status: 400 }, + ); + } + const { title, content, emotionType, diaryDate } = body; if (!content || !diaryDate) { @@ -28,15 +37,39 @@ export async function POST(request: NextRequest) { ); } - const diaryId = await createDiary(userId, { title: title ?? '', content, emotionType: emotionType ?? 'neutral', diaryDate }); - if (!diaryId) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(diaryDate)) { + return NextResponse.json( + { success: false, error: { code: 'VALIDATION_ERROR', message: 'diaryDate 형식이 올바르지 않습니다. (YYYY-MM-DD)' } }, + { status: 400 }, + ); + } + + //실제 존재하는 날짜검증 + const parsedDate = new Date(`${diaryDate}T00:00:00.000Z`); + if (Number.isNaN(parsedDate.getTime()) || parsedDate.toISOString().slice(0, 10) !== diaryDate) { + return NextResponse.json( + { success: false, error: { code: 'VALIDATION_ERROR', message: '유효한 diaryDate가 아닙니다. (YYYY-MM-DD)' } }, + { status: 400 }, + ); + } + + try { + const diaryId = await createDiary(userId, { title: title ?? '', content, emotionType: emotionType ?? 'neutral', diaryDate }); + if (!diaryId) { + return NextResponse.json( + { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, + { status: 500 }, + ); + } + + return NextResponse.json>({ success: true, data: { id: diaryId } }, { status: 201 }); + } catch (error) { + console.error(error); return NextResponse.json( { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, { status: 500 }, ); } - - return NextResponse.json>({ success: true, data: { id: diaryId } }, { status: 201 }); } // GET /api/diary?date=2026-06-16 @@ -105,7 +138,8 @@ export async function GET(request: NextRequest) { success: true, data: { items }, }); - } catch { + } catch (error) { + console.error('[GET /api/diary]', error); return NextResponse.json( { success: false, diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index c8d9ac7..4776adc 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,6 +1,7 @@ 'use client'; import Image from 'next/image'; +import Link from 'next/link'; import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded'; import styles from './Header.module.scss'; @@ -28,13 +29,15 @@ export default function Header(props: HeaderProps) { className={`${styles.header} ${props.transparent ? styles['header--transparent'] : ''}`} >
    - 말해부엉 + + 말해부엉 +
    ); diff --git a/src/domains/diary/diary.service.ts b/src/domains/diary/diary.service.ts index 37ffe08..649869f 100644 --- a/src/domains/diary/diary.service.ts +++ b/src/domains/diary/diary.service.ts @@ -1,4 +1,5 @@ import { prisma } from '@/lib/db'; +import type { EmotionType } from '@/types/diary'; export async function createDiary( userId: string, @@ -21,50 +22,44 @@ export async function createDiary( } } +export type DiaryMutationResult = 'ok' | 'not_found' | 'forbidden' + export async function updateDiaryById( diaryId: string, userId: string, data: { title: string; content: string; emotionType: string }, -): Promise { - try { - const diary = await prisma.emotionDiary.findUnique({ - where: { id: diaryId, deletedAt: null }, - select: { userId: true }, - }); +): Promise { + const diary = await prisma.emotionDiary.findUnique({ + where: { id: diaryId }, + select: { userId: true, deletedAt: true }, + }); - if (!diary || diary.userId !== userId) return false; + if (!diary || diary.deletedAt !== null) return 'not_found'; + if (diary.userId !== userId) return 'forbidden'; - await prisma.emotionDiary.update({ - where: { id: diaryId }, - data, - }); + await prisma.emotionDiary.updateMany({ + where: { id: diaryId, userId, deletedAt: null }, + data, + }); - return true; - } catch (error) { - console.error('[updateDiaryById]', error); - return false; - } + return 'ok'; } -export async function deleteDiaryById(diaryId: string, userId: string): Promise { - try { - const diary = await prisma.emotionDiary.findUnique({ - where: { id: diaryId, deletedAt: null }, - select: { userId: true }, - }); +export async function deleteDiaryById(diaryId: string, userId: string): Promise { + const diary = await prisma.emotionDiary.findUnique({ + where: { id: diaryId }, + select: { userId: true, deletedAt: true }, + }); - if (!diary || diary.userId !== userId) return false; + if (!diary || diary.deletedAt !== null) return 'not_found'; + if (diary.userId !== userId) return 'forbidden'; - await prisma.emotionDiary.update({ - where: { id: diaryId }, - data: { deletedAt: new Date() }, - }); + await prisma.emotionDiary.updateMany({ + where: { id: diaryId, userId, deletedAt: null }, + data: { deletedAt: new Date() }, + }); - return true; - } catch (error) { - console.error('[deleteDiaryById]', error); - return false; - } + return 'ok'; } export async function getDiaryById(diaryId: string, userId: string) { @@ -90,7 +85,7 @@ export async function getDiaryById(diaryId: string, userId: string) { title: diary.title ?? '', content: diary.content, diaryDate: diary.diaryDate.toISOString().slice(0, 10), - emotionType: diary.emotionType ?? null, + emotionType: (diary.emotionType as EmotionType | null) ?? null, createdAt: diary.createdAt.toISOString(), updatedAt: diary.updatedAt.toISOString(), }; diff --git a/src/types/diary.ts b/src/types/diary.ts index f8594bb..44f0928 100644 --- a/src/types/diary.ts +++ b/src/types/diary.ts @@ -56,7 +56,7 @@ export interface DiaryDetail { title: string content: string diaryDate: string - emotionType: string | null + emotionType: EmotionType | null createdAt: string updatedAt: string }