Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6eb7ed6
feat: 홈 화면 기본 구조 생성 (헤더, 일기 박스)
wjdalss21 Jun 17, 2026
f98f8f3
feat: Header variant 분리 (logo/title) 및 홈 화면 적용
wjdalss21 Jun 17, 2026
d959392
feat: 사건기록 페이지 생성 및 로고 헤더 적용 (/records)
wjdalss21 Jun 17, 2026
5a6643e
fix: diaryBox 고정 폭을 max-width + width 100%로 반응형 처리
wjdalss21 Jun 17, 2026
a04b781
feat: 감정일기 작성 버튼 /diary/new 네비게이션 연결
wjdalss21 Jun 17, 2026
6ab4d7e
feat: 홈 화면 캐릭터 이미지 교체 및 위치 조정
wjdalss21 Jun 17, 2026
24aed23
fix: 홈 화면 코드래빗 피드백 반영
wjdalss21 Jun 18, 2026
f67c9e9
refactor: 홈 화면 typography 믹신 적용
wjdalss21 Jun 18, 2026
2b194f9
fix: 비로그인 알아보기 박스 위치 수정
wjdalss21 Jun 18, 2026
e5188f7
feat: 홈 화면에 통계 섹션 및 구분선 통합
wjdalss21 Jun 18, 2026
f2154c6
chore: 개발 편의용 로그인 상태 강제 설정 (배포 전 제거 필요)
wjdalss21 Jun 18, 2026
47e7138
feat: 홈 화면 진행중인 사건 섹션 UI 구현
wjdalss21 Jun 18, 2026
a31fe0e
fix: dev 머지 충돌 해결 (ActiveCasesSection 유지)
wjdalss21 Jun 18, 2026
f02cec2
fix: 진행중인 사건 카드 수정 - CaseCard 원본 구조 유지, 좌측 컬러바 제거, Avatar 좌측 배치
wjdalss21 Jun 18, 2026
e92b35f
fix: CaseCard preview 텍스트 컬러 black-700으로 수정
wjdalss21 Jun 18, 2026
804f450
fix: 진행중인 사건 섹션 내 진행중 뱃지 제거
wjdalss21 Jun 18, 2026
fd1b1f3
fix: 진행중인 사건 카드 푸터 레이아웃 및 외곽선 black-700 2px inset 적용
wjdalss21 Jun 18, 2026
8561f05
fix: 진행중인 사건 카드 preview-footer 간격 8px 조정
wjdalss21 Jun 18, 2026
cca1afc
Merge remote-tracking branch 'origin/dev' into feature/homepage-jm
wjdalss21 Jun 18, 2026
2afe223
fix: 코드래빗 리뷰 반영 - 린트, loading/error 상태 처리, onClick li로 이동
wjdalss21 Jun 18, 2026
cff2117
Merge remote-tracking branch 'origin/dev' into feature/homepage-jm
wjdalss21 Jun 18, 2026
28c7139
chore: 홈 화면 캐릭터 이미지 추가
wjdalss21 Jun 18, 2026
200262e
refactor: 홈 페이지를 /home에서 / 루트 경로로 이동
wjdalss21 Jun 18, 2026
692170e
Merge remote-tracking branch 'origin/dev' into feature/homepage-jm
wjdalss21 Jun 18, 2026
80b320a
Merge remote-tracking branch 'origin/dev' into feature/homepage-jm
wjdalss21 Jun 18, 2026
c5eed1d
Merge remote-tracking branch 'origin/dev' into feature/homepage-jm
wjdalss21 Jun 18, 2026
b6f81f0
Merge remote-tracking branch 'origin/dev' into feature/homepage-jm
wjdalss21 Jun 18, 2026
7937e2c
feat: 홈 화면 새 사건 버튼 및 서비스 정보 푸터 구현
wjdalss21 Jun 18, 2026
12871f4
feat: 새 사건 버튼 카테고리 선택 모달 구현
wjdalss21 Jun 18, 2026
9408df5
fix: CodeRabbit 리뷰 반영 — isLoggedIn 하드코딩 제거, 서비스 링크 경로 교체, SCSS 빈 줄 정리
wjdalss21 Jun 18, 2026
d10f250
Merge remote-tracking branch 'origin/dev' into feature/homepage-jm
wjdalss21 Jun 19, 2026
889d131
feat: HomeGreeting 컴포넌트 추가 및 DB 닉네임 기반 인사말 적용
wjdalss21 Jun 19, 2026
6aa97cf
fix: 통계 카테고리 API 불필요한 인증 체크 제거
wjdalss21 Jun 19, 2026
f63d464
feat: 통계 데이터 없을 때 목 데이터 fallback 추가 (UI 확인용)
wjdalss21 Jun 19, 2026
ed7b7df
chore: 진행중인 사건 목 데이터 제거
wjdalss21 Jun 19, 2026
55e3268
feat: 새 사건 버튼 카테고리 클릭 시 진술 작성 페이지로 이동
wjdalss21 Jun 19, 2026
cd5dfc5
fix: 서비스 소개·고객문의 링크 미연결 항목 span으로 교체
wjdalss21 Jun 19, 2026
83440e7
style: NewCaseButton SCSS declaration-empty-line-before 위반 수정
wjdalss21 Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions src/app/api/statistics/categories/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getSessionUserId } from '@/lib/auth/session'
import { prisma } from '@/lib/db'
import type { ApiResponse } from '@/types/common'

Expand All @@ -14,15 +11,6 @@ const ALL_CATEGORIES = ['ROMANCE', 'FAMILY', 'FRIEND', 'WORK'] as const
// 판결 완료 사건 기준 카테고리별(연애/가족/친구/직장) count 반환
// 비율 계산은 프론트 훅(useStatistics)에서 처리
export async function GET() {
const session = await getServerSession(authOptions)
const userId = getSessionUserId(session)
if (!userId) {
return NextResponse.json<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
{ status: 401 },
)
}

try {
const grouped = await prisma.dispute.groupBy({
by: ['categoryGroup'],
Expand Down
7 changes: 2 additions & 5 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import StatsCategorySection from '@/components/home/StatsCategorySection'
import ActiveCasesSection from '@/components/home/ActiveCasesSection'
import NewCaseButton from '@/components/home/NewCaseButton'
import HomeServiceInfo from '@/components/home/HomeServiceInfo'
import HomeGreeting from '@/components/home/HomeGreeting'
import styles from './page.module.scss'

export default async function HomePage() {
const session = await getServerSession(authOptions)
const isLoggedIn = !!session
const userName = session?.user?.name ?? ''

return (
<div className={styles.page}>
Expand All @@ -29,10 +29,7 @@ export default async function HomePage() {

<main className={styles.container}>
{/* 인사 */}
<section className={styles.greeting}>
<p className={styles.userName}>{isLoggedIn ? `${userName}님` : '안녕하세요'}</p>
<p className={styles.greetingText}>오늘 감정은 어떠신가요?</p>
</section>
<HomeGreeting />

{/* 오늘의 일기 박스 */}
<Link href="/diary/new" className={styles.diaryBox} aria-label="감정일기 작성">
Expand Down
17 changes: 17 additions & 0 deletions src/components/home/HomeGreeting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'

import { useUserMe } from '@/domains/user/hooks'
import styles from '@/app/page.module.scss'

export default function HomeGreeting() {
const { data: user } = useUserMe()

const greeting = user?.nickname ? `${user.nickname}님` : '안녕하세요'

return (
<section className={styles.greeting}>
<p className={styles.userName}>{greeting}</p>
<p className={styles.greetingText}>오늘 감정은 어떠신가요?</p>
</section>
)
}
20 changes: 13 additions & 7 deletions src/components/home/HomeServiceInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import Image from 'next/image'
import styles from './HomeServiceInfo.module.scss'

const LINKS = [
{ label: '서비스 소개', href: '#' },
{ label: '서비스 소개', href: null },
{ label: '개인정보 처리방침', href: '/privacy' },
{ label: '이용약관', href: '/terms' },
{ label: '고객문의', href: '#' },
{ label: '고객문의', href: null },
]

export default function HomeServiceInfo() {
Expand All @@ -19,11 +19,17 @@ export default function HomeServiceInfo() {
className={styles.logo}
/>
<nav className={styles.links} aria-label="서비스 정보">
{LINKS.map(({ label, href }) => (
<a key={label} href={href} className={styles.link}>
{label}
</a>
))}
{LINKS.map(({ label, href }) =>
href ? (
<a key={label} href={href} className={styles.link}>
{label}
</a>
) : (
<span key={label} className={styles.link} aria-disabled="true">
{label}
</span>
),
)}
</nav>
<p className={styles.copyright}>Copyright ⓒ 말해부엉. ALL Rights Reserved.</p>
</div>
Expand Down
5 changes: 0 additions & 5 deletions src/components/home/NewCaseButton.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
position: fixed;
bottom: fn.r(81);
right: max(fn.r(16), calc((100vw - #{v.$container-max-width}) / 2 + #{fn.r(16)}));

display: flex;
align-items: center;
justify-content: center;
width: fn.r(115);
height: fn.r(48);

background-color: v.$color-black-700;
border: none;
border-radius: fn.r(100);
Expand Down Expand Up @@ -43,7 +41,6 @@
position: absolute;
bottom: fn.r(81);
right: max(fn.r(16), calc((100vw - #{v.$container-max-width}) / 2 + #{fn.r(16)}));

display: flex;
flex-direction: column;
align-items: flex-end;
Expand All @@ -57,7 +54,6 @@
border-radius: fn.r(12);
box-sizing: border-box;
padding: fn.r(20) fn.r(16);

display: flex;
flex-direction: column;
align-items: center;
Expand Down Expand Up @@ -85,7 +81,6 @@
background-color: #ffffff;
border: none;
cursor: pointer;

display: flex;
align-items: center;
justify-content: center;
Expand Down
17 changes: 9 additions & 8 deletions src/components/home/NewCaseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import CloseIcon from '@mui/icons-material/Close'
import CategoryIcon from '@/components/ui/CategoryIcon'
import type { CategoryGroup } from '@/types/common'
Expand All @@ -13,20 +14,20 @@ const CATEGORY_ITEMS: { category: CategoryGroup; label: string }[] = [
{ category: 'family', label: '가족관계' },
]

// TODO: 새 사건 작성 페이지(/rooms/new 등) 생성 후 카테고리별 실제 경로로 교체
// 현재는 페이지가 없어 '#'으로 임시 처리
function getCategoryHref(_category: CategoryGroup): string {
return '#'
}

export default function NewCaseButton() {
const [isOpen, setIsOpen] = useState(false)
const router = useRouter()

useEffect(() => {
document.body.style.overflow = isOpen ? 'hidden' : ''
return () => { document.body.style.overflow = '' }
}, [isOpen])

const handleCategoryClick = (category: CategoryGroup) => {
setIsOpen(false)
router.push(`/disputes/test/statement?category=${category}`)
}

return (
<>
{isOpen && (
Expand All @@ -39,10 +40,10 @@ export default function NewCaseButton() {
<div className={styles.panel} onClick={(e) => e.stopPropagation()}>
<div className={styles.categoryBox}>
{CATEGORY_ITEMS.map(({ category, label }) => (
<a key={category} href={getCategoryHref(category)} className={styles.item}>
<button key={category} className={styles.item} onClick={() => handleCategoryClick(category)}>
<CategoryIcon category={category} />
<span className={styles.itemLabel}>{label}</span>
</a>
</button>
))}
</div>
<button
Expand Down
26 changes: 26 additions & 0 deletions src/domains/user/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'

import { useQuery } from '@tanstack/react-query'
import type { ApiResponse } from '@/types/common'

interface UserMeData {
id: string
nickname: string | null
mbti: string | null
}

async function fetchUserMe(): Promise<UserMeData | null> {
const res = await fetch('/api/user/me')
if (res.status === 401) return null
if (!res.ok) throw new Error('사용자 정보를 불러오지 못했습니다.')
const json: ApiResponse<UserMeData> = await res.json()
return json.data ?? null
}

export function useUserMe() {
return useQuery({
queryKey: ['user', 'me'],
queryFn: fetchUserMe,
staleTime: 1000 * 60 * 5,
})
}
36 changes: 2 additions & 34 deletions src/hooks/useActiveCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,50 +27,18 @@ type ActiveDisputeStatus = (typeof ACTIVE_DISPUTE_STATUSES)[number]

type RawCase = ActiveCase & { status: ActiveDisputeStatus }

// TODO: API 연동 시 아래 mock 제거 후 fetch 함수로 교체
// TODO: 진행중인 사건 API 연동 시 활성화
// async function fetchActiveCases(): Promise<ActiveCase[]> {
// const res = await fetch('/api/disputes?active=true')
// if (!res.ok) throw new Error('진행중인 사건을 불러오지 못했습니다.')
// const json = await res.json()
// return json.data.disputes
// }

const MOCK_RAW: RawCase[] = [
{
id: 'mock-1',
categoryGroup: 'romance',
title: '술주정 사건',
description: '제세자 술을 아주 구매하는데 남편은 술을 너무 좋아해서 벌어진 사건',
status: 'waiting_opponent',
createdAt: '2026-06-13T00:00:00Z',
participants: [
{ role: 'role_a', user: { nickname: '근욱', profileImageUrl: null } },
],
},
{
id: 'mock-2',
categoryGroup: 'work',
title: '사내왕따 사건',
description: '신입사원인 제보자가 준법을 하고싶어서 유가를 말했는데 회사분위기 김생쪽에서 나간 사건',
status: 'opponent_joined',
createdAt: '2026-06-11T00:00:00Z',
participants: [
{ role: 'role_a', user: { nickname: '사내', profileImageUrl: null } },
{ role: 'role_b', user: { nickname: '왕따', profileImageUrl: null } },
],
},
]

export function useActiveCases() {
return useQuery({
queryKey: ['disputes', 'active'],
queryFn: async (): Promise<ActiveCase[]> => {
// TODO: 실 API 연동 시 아래 mock 로직 제거 후 fetchActiveCases()로 교체
const active = MOCK_RAW.filter((c) =>
(ACTIVE_DISPUTE_STATUSES as readonly string[]).includes(c.status),
)
return active.map(({ status: _status, ...rest }) => rest)
},
queryFn: async (): Promise<ActiveCase[]> => [],
staleTime: 1000 * 60,
})
}
18 changes: 15 additions & 3 deletions src/hooks/useStatistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export type CategoryStat = CategoryItem & {
percentage: number
}

// TODO: 실제 데이터 쌓이면 제거
const MOCK_ITEMS: CategoryItem[] = [
{ category: 'FRIEND', count: 7 },
{ category: 'ROMANCE', count: 5 },
{ category: 'WORK', count: 4 },
{ category: 'FAMILY', count: 3 },
]

async function fetchCategoryStats() {
const res = await fetch('/api/statistics/categories')
if (!res.ok) throw new Error('통계 데이터를 불러오지 못했습니다.')
Expand All @@ -24,13 +32,17 @@ export function useStatistics() {
queryFn: async () => {
const { items, total } = await fetchCategoryStats()

const source = total === 0 ? MOCK_ITEMS : items
const mockTotal = MOCK_ITEMS.reduce((s, i) => s + i.count, 0)
const resolvedTotal = total === 0 ? mockTotal : total

// 카테고리별 비율 계산 (소수점 1자리)
const stats: CategoryStat[] = items.map((item) => ({
const stats: CategoryStat[] = source.map((item) => ({
...item,
percentage: total > 0 ? Math.round((item.count / total) * 1000) / 10 : 0,
percentage: Math.round((item.count / resolvedTotal) * 1000) / 10,
}))

return { items: stats, total }
return { items: stats, total: resolvedTotal }
},
staleTime: 1000 * 60 * 60 * 24, // 서버 revalidate(86400s)와 맞춤
})
Expand Down