Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 108 additions & 0 deletions src/app/(page)/home/page.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
@use '@/styles/abstracts/variables' as v;

.page {
position: relative;
}

.characterImage {
position: absolute;
top: 41px;
right: -20px;
width: 169px;
height: 138px;
object-fit: contain;
pointer-events: none;
z-index: 0;
}

.container {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
padding: 20px 16px;
gap: 16px;
}

// 인사
.greeting {
display: flex;
flex-direction: column;
gap: 4px;
}

.userName {
font-size: v.$font-size-title-m;
font-weight: v.$font-weight-bold;
color: v.$color-black-700;
line-height: v.$line-height-title-m;
}

.greetingText {
font-size: v.$font-size-body-s;
font-weight: v.$font-weight-regular;
color: v.$color-black-400;
line-height: v.$line-height-body-s;
}

// 일기 박스
.diaryBox {
display: flex;
justify-content: space-between;
align-items: center;
align-self: center;
width: 100%;
max-width: 365px;
height: 88px;
padding: 20px 16px;
border: 2px solid v.$color-black-700;
border-radius: 12px;
background-color: v.$color-primary-100;
box-sizing: border-box;
text-decoration: none;
cursor: pointer;
}

.diaryTextGroup {
display: flex;
flex-direction: column;
gap: 4px;
}

.diaryTitle {
font-size: v.$font-size-body-s;
font-weight: v.$font-weight-semibold;
color: v.$color-black-700;
line-height: v.$line-height-body-s;
}

.diarySubtitle {
font-size: v.$font-size-caption;
font-weight: v.$font-weight-regular;
color: v.$color-black-400;
line-height: v.$line-height-caption;
}

// 구분선
.divider {
height: 8px;
background-color: v.$color-black-100;
margin-top: 26px; // gap(16) + 26 = 42px
margin-left: -16px;
margin-right: -16px;
}

.diaryButton {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: v.$font-weight-bold;
color: v.$color-black-700;
background: none;
border: none;
cursor: pointer;
flex-shrink: 0;
}
50 changes: 50 additions & 0 deletions src/app/(page)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Image from 'next/image'
import Link from 'next/link'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import Header from '@/components/layout/Header'
import StatsCategorySection from '@/components/home/StatsCategorySection'
import styles from './page.module.scss'

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

return (
<div className={styles.page}>
<Header variant="logo" />

<Image
src="/images/characters/character-home.png"
alt="말해부엉 캐릭터"
width={169}
height={138}
className={styles.characterImage}
/>

<main className={styles.container}>
{/* 인사 */}
<section className={styles.greeting}>
<p className={styles.userName}>{userName}님</p>
<p className={styles.greetingText}>오늘 감정은 어떠신가요?</p>
</section>

{/* 오늘의 일기 박스 */}
{/* 승규님 우선 버튼 클릭시 diary/new로 작성페이지를 임시로 지정해놨는데 작성페이지 이름 어떻게 하실건지 알려주시면 수정하겠습니다 */}
<Link href="/diary/new" className={styles.diaryBox} aria-label="감정일기 작성">
<div className={styles.diaryTextGroup}>
<p className={styles.diaryTitle}>오늘의 일기를 적어보세요</p>
<p className={styles.diarySubtitle}>감정일기 작성하러가기</p>
</div>
<span className={styles.diaryButton} aria-hidden="true">+</span>
</Link>

{/* 고민 카테고리 TOP4 */}
<StatsCategorySection />

{/* 구분선 */}
<div className={styles.divider} />
</main>
</div>
)
}
11 changes: 11 additions & 0 deletions src/app/(page)/records/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Header from '@/components/layout/Header'

export default function RecordsPage() {
return (
<>
<Header variant="logo" />
<main>
</main>
</>
)
}
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import QueryProvider from '@/components/providers/QueryProvider';
import './globals.scss';

const pretendard = localFont({
Expand All @@ -22,7 +23,9 @@ export default function RootLayout({
return (
<html lang="ko" className={pretendard.variable}>
<body>
<main className="container">{children}</main>
<QueryProvider>
<main className="container">{children}</main>
</QueryProvider>
</body>
</html>
);
Expand Down
84 changes: 84 additions & 0 deletions src/components/home/StatsCategorySection.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
@use '@/styles/abstracts/variables' as v;
@use '@/styles/abstracts/functions' as fn;

.section {
display: flex;
flex-direction: column;
gap: fn.r(12);
margin-top: fn.r(24); // container gap(16) + 24 = 40 (일기박스 ↔ 타이틀)
}

.title {
font-size: v.$font-size-title-s;
font-weight: v.$font-weight-bold;
line-height: v.$line-height-title-s;
color: var(--text-primary);
}

// 단일 스택 바
.bar {
display: flex;
width: fn.r(346);
height: fn.r(12);
align-self: center;
overflow: hidden;
border-radius: 0;

&__segment {
height: 100%;

&--romance { background-color: var(--category-love-bg); }
&--work { background-color: var(--category-work-bg); }
&--friend { background-color: var(--category-friend-bg); }
&--family { background-color: var(--category-family-bg); }
&--placeholder { background-color: var(--bg-disabled); }
}
}

// 카테고리 레이블 2×2 그리드
.labels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: fn.r(8) fn.r(0);
margin-top: fn.r(1); // bar ↔ labels: 13px (gap 12 + 1 = 13)
justify-items: center;
}

.label {
display: flex;
align-items: center;
gap: fn.r(4);

&__icon {
font-size: fn.r(14) !important;

&--romance { color: var(--category-love-text); }
&--work { color: var(--category-work-text); }
&--friend { color: var(--category-friend-text); }
&--family { color: var(--category-family-text); }
}

&__text {
font-size: v.$font-size-caption;
font-weight: v.$font-weight-regular;
line-height: v.$line-height-caption;

&--romance { color: var(--category-love-text); }
&--work { color: var(--category-work-text); }
&--friend { color: var(--category-friend-text); }
&--family { color: var(--category-family-text); }

&--bold { font-weight: v.$font-weight-bold; }
}

&__percent {
font-size: v.$font-size-caption;
font-weight: v.$font-weight-regular;
line-height: v.$line-height-caption;

&--romance { color: var(--category-love-text); }
&--work { color: var(--category-work-text); }
&--friend { color: var(--category-friend-text); }
&--family { color: var(--category-family-text); }
}
}
73 changes: 73 additions & 0 deletions src/components/home/StatsCategorySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';

import FavoriteIcon from '@mui/icons-material/Favorite';
import BusinessCenterIcon from '@mui/icons-material/BusinessCenter';
import Diversity3Icon from '@mui/icons-material/Diversity3';
import FamilyRestroomIcon from '@mui/icons-material/FamilyRestroom';
import { useStatistics } from '@/hooks/useStatistics';
import styles from './StatsCategorySection.module.scss';

type CategoryKey = 'ROMANCE' | 'WORK' | 'FRIEND' | 'FAMILY';

const CATEGORY_CONFIG: Record<CategoryKey, { label: string; icon: React.ElementType; styleKey: string }> = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 목적: React 네임스페이스 타입 사용 패턴과 TS 설정을 확인해 타입 호환성 검증
fd -a tsconfig.json
echo "---- tsconfig excerpt ----"
sed -n '1,220p' tsconfig.json
echo "---- React namespace type usage ----"
rg -n "React\\.ElementType|React\\.ReactNode|React\\." src
echo "---- explicit ElementType imports ----"
rg -n "import type \\{[^}]*ElementType[^}]*\\} from 'react'" src

Repository: I5-Project/TALKY-OWL

Length of output: 2197


🏁 Script executed:

cat -n src/components/home/StatsCategorySection.tsx | head -30

Repository: I5-Project/TALKY-OWL

Length of output: 1548


🏁 Script executed:

cat -n src/components/ui/CategoryFilter.tsx | head -30

Repository: I5-Project/TALKY-OWL

Length of output: 1413


🏁 Script executed:

# Check if React is imported in the StatsCategorySection file
rg -n "import.*React" src/components/home/StatsCategorySection.tsx

Repository: I5-Project/TALKY-OWL

Length of output: 46


🏁 Script executed:

# Check for ambient React declarations or global type definitions
rg -n "declare.*React|React.*=" src --type ts --type tsx | head -20

Repository: I5-Project/TALKY-OWL

Length of output: 92


🏁 Script executed:

# Check next-env.d.ts which might provide ambient React types
cat -n next-env.d.ts

Repository: I5-Project/TALKY-OWL

Length of output: 368


🏁 Script executed:

# Check React version and setup
cat -n package.json | grep -A 5 '"react"'

Repository: I5-Project/TALKY-OWL

Length of output: 263


React.ElementType 타입을 명시적 import로 변경하세요.

파일에서 React 네임스페이스를 import하지 않으면서 React.ElementType을 사용하고 있습니다. 현재 TypeScript 설정에서는 타입 해석 오류가 발생할 수 있습니다. 다음과 같이 수정하세요:

수정 예시
+import type { ElementType } from 'react';
 import FavoriteIcon from '`@mui/icons-material/Favorite`';
 import BusinessCenterIcon from '`@mui/icons-material/BusinessCenter`';
 import Diversity3Icon from '`@mui/icons-material/Diversity3`';
 import FamilyRestroomIcon from '`@mui/icons-material/FamilyRestroom`';
 import { useStatistics } from '`@/hooks/useStatistics`';
 import styles from './StatsCategorySection.module.scss';

 type CategoryKey = 'ROMANCE' | 'WORK' | 'FRIEND' | 'FAMILY';

-const CATEGORY_CONFIG: Record<CategoryKey, { label: string; icon: React.ElementType; styleKey: string }> = {
+const CATEGORY_CONFIG: Record<CategoryKey, { label: string; icon: ElementType; styleKey: string }> = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const CATEGORY_CONFIG: Record<CategoryKey, { label: string; icon: React.ElementType; styleKey: string }> = {
import type { ElementType } from 'react';
import FavoriteIcon from '`@mui/icons-material/Favorite`';
import BusinessCenterIcon from '`@mui/icons-material/BusinessCenter`';
import Diversity3Icon from '`@mui/icons-material/Diversity3`';
import FamilyRestroomIcon from '`@mui/icons-material/FamilyRestroom`';
import { useStatistics } from '`@/hooks/useStatistics`';
import styles from './StatsCategorySection.module.scss';
type CategoryKey = 'ROMANCE' | 'WORK' | 'FRIEND' | 'FAMILY';
const CATEGORY_CONFIG: Record<CategoryKey, { label: string; icon: ElementType; styleKey: string }> = {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/home/StatsCategorySection.tsx` at line 12, The CATEGORY_CONFIG
constant uses React.ElementType without properly importing the React namespace,
which can cause TypeScript resolution errors. Add an explicit import of
ElementType from the react package at the top of the file, and then replace
React.ElementType with just ElementType in the type annotation for the icon
property within the CATEGORY_CONFIG Record definition.

ROMANCE: { label: '연인관계', icon: FavoriteIcon, styleKey: 'romance' },
WORK: { label: '직장관계', icon: BusinessCenterIcon, styleKey: 'work' },
FRIEND: { label: '친구관계', icon: Diversity3Icon, styleKey: 'friend' },
FAMILY: { label: '가족관계', icon: FamilyRestroomIcon, styleKey: 'family' },
};

const PLACEHOLDER_CATEGORIES = ['ROMANCE', 'WORK', 'FRIEND', 'FAMILY'] as const;

export default function StatsCategorySection() {
const { data, isLoading, isError } = useStatistics();

const isEmpty = !data || data.total === 0;
const showPlaceholder = isLoading || isError || isEmpty;

Comment on lines +24 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

401 에러 시 null 렌더링 요구사항과 현재 분기가 충돌합니다.

현재 isError를 플레이스홀더 렌더링으로 처리해서, 인증 실패(401)에서도 섹션이 표시됩니다. PR 목표대로라면 인증 실패 시 null 반환 분기를 분리해야 합니다.

수정 예시
 export default function StatsCategorySection() {
   const { data, isLoading, isError } = useStatistics();

   const isEmpty = !data || data.total === 0;
-  const showPlaceholder = isLoading || isError || isEmpty;
+  if (isError) return null;
+  const showPlaceholder = isLoading || isEmpty;

Also applies to: 27-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/home/StatsCategorySection.tsx` around lines 24 - 26, The
current logic in StatsCategorySection.tsx treats all errors the same by
including isError in the showPlaceholder condition, but the requirement is to
return null specifically for 401 authentication errors. Separate the error
handling logic by first checking if the error is a 401 authentication failure
and return null directly from the component in that case, then only treat other
errors as part of the showPlaceholder condition. This ensures authentication
failures do not render the section at all, while other errors can display a
placeholder.

return (
<section className={styles.section}>
<h2 className={styles.title}>고민 카테고리 TOP4</h2>

{/* 단일 스택 바: 데이터 없거나 로딩/에러면 25%씩 회색 플레이스홀더 */}
<div className={styles.bar}>
{showPlaceholder
? PLACEHOLDER_CATEGORIES.map((category) => (
<div
key={category}
className={`${styles.bar__segment} ${styles['bar__segment--placeholder']}`}
style={{ width: '25%' }}
/>
))
: data!.items.map(({ category, percentage }) => {
const { styleKey } = CATEGORY_CONFIG[category as CategoryKey];
return (
<div
key={category}
className={`${styles.bar__segment} ${styles[`bar__segment--${styleKey}`]}`}
style={{ width: `${percentage}%` }}
/>
);
})}
</div>

{/* 카테고리 레이블 2×2 그리드: 아이콘 + 이름(볼드) + 퍼센트 */}
<div className={styles.labels}>
{PLACEHOLDER_CATEGORIES.map((category) => {
const { label, icon: Icon, styleKey } = CATEGORY_CONFIG[category];
const percentage = showPlaceholder ? null : data!.items.find((i) => i.category === category)?.percentage ?? 0;
return (
<div key={category} className={styles.label}>
<Icon className={`${styles.label__icon} ${styles[`label__icon--${styleKey}`]}`} />
<span className={`${styles.label__text} ${styles[`label__text--${styleKey}`]} ${styles['label__text--bold']}`}>
{label}
</span>
<span className={`${styles.label__percent} ${styles[`label__percent--${styleKey}`]}`}>
{percentage === null ? '-' : `${percentage}%`}
</span>
</div>
);
})}
</div>
</section>
);
}
7 changes: 7 additions & 0 deletions src/components/layout/Header.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
width: 100%;
background-color: var(--bg-page);

&__logo {
display: flex;
align-items: center;
height: fn.r(56);
padding: fn.r(16) fn.r(20);
}

&__nav {
display: flex;
align-items: center;
Expand Down
Loading