Skip to content
Merged
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
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const eslintConfig = [
'unused-imports': unusedImports,
},
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
'react/jsx-sort-props': [
'warn',
{
Expand Down
12 changes: 11 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
--color-blog-blue: var(--blog-blue);
--color-blog-green: var(--blog-green);
--color-blog-yellow: var(--blog-yellow);
--color-blog-black: var(--blog-black);
--color-blog-gray-600: var(--blog-gray-600);
--color-blog-gray-500: var(--blog-gray-500);
--color-blog-gray-400: var(--blog-gray-400);
--color-blog-gray-300: var(--blog-gray-300);
Expand Down Expand Up @@ -62,6 +64,11 @@
--height-content: calc(
100vh - var(--height-header) - 2 * var(--header-padding-y)
);
--height-footer: var(--footer-height);
--padding-footer: var(--footer-height) 0;
--line-height-footer: calc(
var(--footer-height) - 2 * var(--footer-padding-x)
);
Comment on lines +69 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

--line-height-footer를 계산할 때 정의되지 않은 --footer-padding-x 변수를 사용하고 있어 CSS가 올바르게 적용되지 않을 수 있습니다. 이 변수를 정의하거나, 계산에서 제거하는 것을 고려해 보세요. 만약 수평 패딩이 없다면, calc(var(--footer-height))로도 충분할 수 있습니다.

}

:root {
Expand All @@ -70,11 +77,14 @@
--header-padding-x: 1rem;
--header-padding-y: 3rem;
--card: oklch(1 0 0);
--footer-height: 92px;
--blog-purple: oklch(0.6095 0.2382 332.15);
--blog-pink: oklch(0.5717 0.2063 13.44);
--blog-blue: oklch(0.5332 0.2106 263.54);
--blog-green: oklch(0.684 0.1136 154.11);
--blog-yellow: oklch(0.7266 0.1318 91.46);
--blog-black: oklch(0.3496 0.0141 274.5);
--blog-gray-600: oklch(0.6428 0.0152 248.06);
--blog-gray-500: oklch(0.8196 0.0047 258.33);
--blog-gray-400: oklch(0.8779 0.0026 228.79);
--blog-gray-300: oklch(0.9401 0 0);
Expand Down Expand Up @@ -158,6 +168,6 @@
@apply border-border break-keep outline-ring/50;
}
body {
@apply flex items-center justify-center font-pretendard;
@apply flex items-center justify-center bg-blog-gray-100 font-pretendard;
}
}
40 changes: 24 additions & 16 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { Button, PostCard } from '@/shared';
'use client';

import { useState } from 'react';

import { PostCard, Tabs } from '@/shared';
import { Grid } from '@/widgets';

export default function Home() {
const posts = Array.from({ length: 9 }); // 임시 데이터

const [search, setSearch] = useState('');

const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
Comment on lines +11 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

search 상태와 onChangeSearch 핸들러가 선언되었지만 컴포넌트 내에서 사용되지 않고 있습니다. 현재 사용하지 않는 코드라면 삭제하여 코드베이스를 깔끔하게 유지하는 것이 좋습니다.

return (
<div className='flex w-full flex-col items-start justify-start'>
<div className='flex gap-3 py-2'>
<Button className='px-2 font-bold'>전체 (49)</Button>
<Button className='px-2 font-bold' variant='ghost'>
개발 (3)
</Button>
<Button className='px-2 font-bold' variant='ghost'>
일상 (9)
</Button>
<Button className='px-2 font-bold' variant='ghost'>
회고 (4)
</Button>
</div>
<Grid columns={{ base: 1, md: 2, lg: 4 }} gap={16}>
<PostCard />
<div className='flex flex-col items-center justify-center gap-10'>
<Tabs />
<Grid
className='w-full px-4 md:px-8 lg:px-0'
cols='grid-cols-1 md:grid-cols-2 lg:grid-cols-[270px_270px_270px]'
gap={5}
justifyContent='center'
maxWidth='7xl'
>
{posts.map((_, index) => (
<PostCard key={index} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

map 함수에서 배열의 indexkey로 사용하고 있습니다. 현재는 임시 데이터라 문제가 되지 않지만, 추후 실제 데이터로 변경될 때 리스트 아이템이 추가, 삭제, 또는 재정렬될 경우 성능 문제나 상태 관리의 버그를 유발할 수 있습니다. 각 포스트의 고유 ID(예: post.id)를 key로 사용하는 것을 권장합니다.

))}
</Grid>
</div>
);
Expand Down
3 changes: 0 additions & 3 deletions src/shared/components/features/Footer.tsx

This file was deleted.

20 changes: 0 additions & 20 deletions src/shared/components/features/Header.tsx

This file was deleted.

29 changes: 0 additions & 29 deletions src/shared/components/features/PostCard.tsx

This file was deleted.

34 changes: 34 additions & 0 deletions src/shared/components/features/card/PostCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Image from 'next/image';

import { CalendarDays } from 'lucide-react';

export const PostCard = () => {
return (
<div className='flex w-full flex-col overflow-hidden bg-white shadow-lg transform-content'>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

transform-content는 유효한 Tailwind CSS 클래스가 아닙니다. 아마 transform을 의도하신 것 같습니다. transform 클래스는 scale, rotate 등 다른 변환 유틸리티와 함께 사용될 때 활성화됩니다. 의도하신 기능이 없다면 이 클래스를 제거하는 것이 좋습니다.

Suggested change
<div className='flex w-full flex-col overflow-hidden bg-white shadow-lg transform-content'>
<div className='flex w-full flex-col overflow-hidden bg-white shadow-lg'>

<div className='relative aspect-video w-full'>
<Image
fill
alt='thumbnail'
className='object-cover'
src='https://picsum.photos/400/225'
/>
</div>
<div className='flex h-full w-full flex-col justify-between p-3'>
<div className='flex flex-col gap-2'>
<div className='text-sm font-bold text-blog-pink'>일상</div>
<div>게시물 1 제목제목</div>
<div className='text-sm text-blog-blue'>
게시물 1에 대한 내용입니다.게시물 1에 대한 내용입니다.게시물 1에
대한 내용입니다.게시물 1에 대한 내용입니다.
</div>
Comment on lines +20 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

게시물 내용이 길어지면 카드의 높이가 달라져 그리드 레이아웃이 깨질 수 있습니다. 일관된 UI를 위해 truncate 클래스를 사용해 한 줄로 표시하거나, @tailwindcss/line-clamp 플러그인을 사용하여 여러 줄 말줄임 처리를 하는 것을 고려해 보세요.

Suggested change
<div className='text-sm text-blog-blue'>
게시물 1 대한 내용입니다.게시물 1 대한 내용입니다.게시물 1
대한 내용입니다.게시물 1 대한 내용입니다.
</div>
<div className='truncate text-sm text-blog-blue'>
게시물 1 대한 내용입니다.게시물 1 대한 내용입니다.게시물 1
대한 내용입니다.게시물 1 대한 내용입니다.
</div>

</div>
<div className='flex w-full items-center gap-2 pt-3'>
<CalendarDays className='size-5 text-blog-gray-600' />
<p className='text-sm font-medium text-blog-gray-600'>
2025년 1월 14일
</p>
</div>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/shared/components/features/card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PostCard';
6 changes: 3 additions & 3 deletions src/shared/components/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './Header';
export * from './Footer';
export * from './PostCard';
export * from './card';
export * from './layout';
export * from './tabs';
32 changes: 32 additions & 0 deletions src/shared/components/features/layout/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Link from 'next/link';

export const Footer = () => {
return (
<footer className='flex h-footer w-full items-center justify-center bg-blog-gray-300'>
<div className='flex flex-col items-start justify-between gap-1'>
<p className='text-blog-gray-600'>
Copyright &copy; 2025 <b>Dobbymin</b> All rights reserved.
</p>
<div className='flex items-center justify-center gap-1 font-fira-code text-blog-gray-600'>
<span>gmin0701@knu.ac.kr</span>
<span className='font-bold'>.</span>
<span>+82-10-3095-7259</span>
<span className='font-bold'>.</span>
<Link
className='hover:font-bold hover:underline'
href='https://github.com/Dobbymin'
>
GitHub
</Link>
<span className='font-bold'>.</span>
<Link
className='hover:font-bold hover:underline'
href='https://www.linkedin.com/in/dobbymin/'
>
LinkedIn
</Link>
</div>
Comment on lines +8 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

푸터에 개인 정보(이메일, 전화번호, 링크)가 하드코딩되어 있습니다. 이 정보들을 별도의 상수 파일로 분리하여 관리하면 재사용성과 유지보수성을 높일 수 있습니다.

또한, <b> 태그 대신 Tailwind CSS의 font-bold 클래스를 사용하는 것이 일관성 측면에서 더 좋습니다.

예시:

// constants/author.ts
export const authorInfo = {
  name: 'Dobbymin',
  email: 'gmin0701@knu.ac.kr',
  // ...
};

// Footer.tsx
<p>...
  Copyright &copy; 2025 <span className='font-bold'>{authorInfo.name}</span> All rights reserved.
</p>

</div>
</footer>
);
};
29 changes: 29 additions & 0 deletions src/shared/components/features/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Link from 'next/link';

import { Search, Sun } from 'lucide-react';

import { routerPath } from '../../../constants';
import { Input } from '../../ui';

export const Header = () => {
return (
<header className='fixed top-0 right-0 left-0 z-100 flex h-header items-center justify-between bg-blog-gray-500 px-header transition-colors'>
<Link href={routerPath.main}>
<p className='font-lemon text-3xl'>Dobblog</p>
</Link>
<div className='absolute top-1/2 left-1/2 hidden w-[400px] max-w-[60vw] -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-3 rounded-sm bg-blog-gray-100 md:flex'>
<Input
className='border-none bg-transparent text-sm text-blog-gray-500 shadow-none outline-none placeholder:text-blog-gray-400 focus:border-none focus:ring-0 focus:outline-none'
type='text'
/>
Comment on lines +15 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

검색 Input 컴포넌트에 placeholder가 없어 사용자에게 어떤 입력이 필요한지 알려주기 어렵습니다. placeholder='검색어를 입력하세요...'와 같이 안내 문구를 추가하면 사용자 경험을 개선할 수 있습니다.

Suggested change
<Input
className='border-none bg-transparent text-sm text-blog-gray-500 shadow-none outline-none placeholder:text-blog-gray-400 focus:border-none focus:ring-0 focus:outline-none'
type='text'
/>
<Input
className='border-none bg-transparent text-sm text-blog-gray-500 shadow-none outline-none placeholder:text-blog-gray-400 focus:border-none focus:ring-0 focus:outline-none'
placeholder='검색어를 입력하세요...'
type='text'
/>

<Search className='mr-3 text-blog-gray-500' size={20} />
</div>
<div className='flex items-center justify-between gap-5'>
<Link href={routerPath.about}>
<p className='font-lemon text-xl'>About</p>
</Link>
<Sun className='cursor-pointer text-blog-pink' />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Sun 아이콘에 cursor-pointer 클래스가 적용되어 있어 사용자가 클릭할 수 있는 요소로 보이지만, 실제로는 onClick 핸들러가 연결되어 있지 않습니다. 이는 사용자에게 혼란을 줄 수 있으며, 키보드 사용자 등에게는 접근성이 떨어집니다. 상호작용을 의도하셨다면 button 태그로 감싸고 onClick 핸들러를 추가하는 것이 웹 접근성 모범 사례입니다.

</div>
</header>
);
};
2 changes: 2 additions & 0 deletions src/shared/components/features/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Footer';
export * from './Header';
33 changes: 33 additions & 0 deletions src/shared/components/features/tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import { useState } from 'react';

export const Tabs = () => {
const [selected, setSelected] = useState(0);

const tabs = [
{ name: 'All.tsx', count: 49 },
{ name: 'Develop.tsx', count: 3 },
{ name: 'Daily.tsx', count: 9 },
{ name: 'Review.tsx', count: 4 },
];

return (
<div className='flex h-13 justify-start overflow-x-auto py-2 font-fira-code'>
{tabs.map((tab, idx) => (
<div
key={tab.name}
className={`flex cursor-pointer items-center justify-center gap-2 border-r px-5 text-sm font-semibold transition-colors last:border-r-0 ${
selected === idx
? 'bg-blog-gray-100 text-blog-black'
: 'bg-blog-gray-200 text-blog-gray-600 hover:font-bold'
} ${idx === 0 ? '' : 'w-full'} `}
onClick={() => setSelected(idx)}
>
<p>{tab.name}</p>
<p className='text-blog-green'>({tab.count})</p>
</div>
))}
</div>
Comment on lines +16 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

현재 Tabs 컴포넌트 구현이 PR 설명과 다르고, 웹 접근성 및 스타일링에 개선이 필요해 보입니다.

  1. 웹 접근성: 클릭 가능한 div 대신 시맨틱한 <button> 태그와 role='tab' 속성을 사용해야 키보드 탐색과 스크린 리더 호환성을 보장할 수 있습니다.
  2. 구현 불일치: PR 설명에 언급된 반응형 패딩(px-3 sm:px-5), 최소 너비(min-w-[120px]), 스냅 스크롤(snap-x) 등의 기능이 누락되었습니다.
  3. 스타일링 문제: h-13은 유효한 Tailwind 클래스가 아니며, w-full 로직은 스크롤 가능한 탭의 의도와 맞지 않아 보입니다.

아래와 같이 개선하는 것을 제안합니다.

    <div className='snap-x snap-mandatory flex h-[52px] w-full justify-start overflow-x-auto py-2 font-fira-code'>
      {tabs.map((tab, idx) => (
        <button
          key={tab.name}
          role="tab"
          aria-selected={selected === idx}
          className={`snap-start flex min-w-[120px] shrink-0 cursor-pointer items-center justify-center gap-2 border-r px-3 text-sm font-semibold transition-colors last:border-r-0 sm:px-5 ${
            selected === idx
              ? 'bg-blog-gray-100 text-blog-black'
              : 'bg-blog-gray-200 text-blog-gray-600 hover:font-bold'
          }`}
          onClick={() => setSelected(idx)}
        >
          <p className="truncate">{tab.name}</p>
          <p className='text-blog-green'>({tab.count})</p>
        </button>
      ))}
    </div>

);
};
1 change: 1 addition & 0 deletions src/shared/components/features/tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Tabs';
4 changes: 4 additions & 0 deletions src/shared/types/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.css' {
const content: { [className: string]: string };
export default content;
}
60 changes: 31 additions & 29 deletions src/widgets/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,51 @@ import * as React from 'react';

import { cn } from '@/shared';

type ResponsiveGrid = Partial<{
base: number;
sm: number;
md: number;
lg: number;
xl: number;
'2xl': number;
}>;

type Props = {
columns: number | ResponsiveGrid;
cols: string;
gap?: number;
maxWidth?:
| 'xs'
| 'sm'
| 'md'
| 'lg'
| 'xl'
| '2xl'
| '3xl'
| '4xl'
| '5xl'
| '6xl'
| '7xl'
| 'full'
| 'none';
justifyContent?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
} & React.HTMLAttributes<HTMLDivElement>;

export const Grid = ({
children,
columns,
gap = 0,
cols,
gap,
maxWidth,
justifyContent,
className,
...props
}: Props) => {
const gridColsClass = getGridColsClass(columns);
const gapClass = `gap-[${gap}px]`;

const gapClass = gap ? `gap-${gap}` : '';
const maxWidthClass = maxWidth ? `max-w-${maxWidth}` : '';
const justifyContentClass = justifyContent ? `justify-${justifyContent}` : '';
return (
<div
className={cn('grid w-full', gridColsClass, gapClass, className)}
className={cn(
'grid w-full',
cols,
gapClass,
maxWidthClass,
justifyContentClass,
className,
)}
{...props}
>
{children}
</div>
);
};

function getGridColsClass(columns: number | ResponsiveGrid) {
if (typeof columns === 'number') {
return `grid-cols-${columns}`;
}

return Object.entries(columns)
.map(([breakpoint, count]) => {
const prefix = breakpoint === 'base' ? '' : `${breakpoint}:`;
return `${prefix}grid-cols-${count}`;
})
.join(' ');
}
Loading