Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
20b595b
fix: 두가지 버그 픽스
Hyeonjun0527 Feb 1, 2026
fbfddc4
Merge pull request #359 from code-zero-to-one/fix/new-feature-bug
Hyeonjun0527 Feb 1, 2026
d943df1
fix: 각종 버그 수정
Hyeonjun0527 Feb 1, 2026
87be95b
Merge pull request #361 from code-zero-to-one/release/new-feature-bug…
Hyeonjun0527 Feb 1, 2026
776874b
fix: 스크롤 문제 해결
Hyeonjun0527 Feb 1, 2026
4f9ef70
Merge pull request #363 from code-zero-to-one/fix/scroll
Hyeonjun0527 Feb 1, 2026
59514c0
fix: 스레드 및 댓글 조회 기능 개선, 페이지네이션 추가 및 오류 처리 메시지 개선
HA-SEUNG-JEONG Feb 2, 2026
d8344a8
fix: 버튼 컴포넌트로 변경하여 오류 처리 메시지에 다시 시도 버튼 추가
HA-SEUNG-JEONG Feb 2, 2026
dd67f74
fix: 댓글 작성 후 페이지를 초기화하는 코드 제거
HA-SEUNG-JEONG Feb 2, 2026
c906fc5
fix: 댓글 조회 기능 개선 및 페이지네이션 추가, 댓글 응답 타입 수정
HA-SEUNG-JEONG Feb 2, 2026
e7d7012
Merge pull request #366 from code-zero-to-one/fix-mission
HA-SEUNG-JEONG Feb 2, 2026
05e8327
chore: 코드래빗
Hyeonjun0527 Feb 3, 2026
716e8ce
chore:코드래빗 2
Hyeonjun0527 Feb 3, 2026
1f37be9
fix : 그룹/멘토스터디 기본값을 '모집중' 으로
seong-jin-jo Feb 3, 2026
9f6cc59
chore : prettier
seong-jin-jo Feb 3, 2026
41b1313
chore : prettier 에서 corderabbit 제외
seong-jin-jo Feb 3, 2026
92e916a
feat : 그룹스터디/멘토스터디에서 내가 참여중인 스터디목록 조회
seong-jin-jo Feb 3, 2026
c396df8
chore : 나의 소중한 스터디로 정정
seong-jin-jo Feb 3, 2026
9237593
chore : CI
seong-jin-jo Feb 3, 2026
31be1a8
Merge pull request #370 from code-zero-to-one/feat/SRPINT3/enhance-jo…
seong-jin-jo Feb 3, 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
89 changes: 89 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "ko-KR"
early_access: false

reviews:
profile: "chill"
request_changes_workflow: false
high_level_summary: true
high_level_summary_placeholder: "@coderabbitai summary"
poem: true
review_status: true
review_details: false

# 자동 리뷰 설정
auto_review:
enabled: true
# drafts: false = 드래프트 PR은 리뷰하지 않음
# drafts: true = 드래프트 PR도 리뷰함 (모든 브랜치에 적용되는 게 아니라 드래프트 PR 포함 여부)
drafts: true
auto_incremental_review: true
base_branches:
- develop
- main

# 경로 필터 (필요시 조정)
path_filters: []

# 도구 설정
tools:
# ESLint: JavaScript/TypeScript 린터 (프로젝트에서 사용 중)
eslint:
enabled: true
# Biome: 빠른 포맷터/린터 (프로젝트에서 사용 안 함 - 비활성화 권장)
biome:
enabled: false
# Markdownlint: Markdown 파일 린터
markdownlint:
enabled: true
# GitHub Checks: GitHub의 체크 상태 표시
github-checks:
enabled: true
# Gitleaks: 시크릿(API 키, 비밀번호 등) 유출 검사
gitleaks:
enabled: true
# OSV Scanner: 오픈소스 취약점 스캐너
osvScanner:
enabled: true

chat:
auto_reply: true
art: true

# 지식 베이스 설정
knowledge_base:
opt_out: false
web_search:
enabled: true
code_guidelines:
enabled: true
# 코드 가이드라인 파일 패턴 (실제 존재하는 파일만 포함)
filePatterns:
- "**/CLAUDE.md" # ✅ 존재함 (루트에 있음)
# - "**/.cursorrules" # ❌ 존재하지 않음
# - ".github/copilot-instructions.md" # ❌ 존재하지 않음
# - ".github/instructions/*.instructions.md" # ❌ 존재하지 않음
pull_requests:
scope: "auto"
issues:
scope: "auto"

# 코드 생성 설정
code_generation:
docstrings:
language: "ko-KR"
path_instructions: []
unit_tests:
path_instructions: []

# 이슈 풍부화 설정
issue_enrichment:
planning:
enabled: true
auto_planning:
enabled: true
labels: []
labeling:
auto_apply_labels: false
labeling_instructions: []

5 changes: 4 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ build/
# OpenAPI auto-generated files
src/api/openapi

**/*.json
**/*.json

# CodeRabbit config
.coderabbit.yaml
14 changes: 13 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { NextConfig } from 'next';
import type { RemotePattern } from 'next/dist/shared/lib/image-config';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "next.config.ts" -o -name "next.config.js" | head -5

Repository: code-zero-to-one/study-platform-client

Length of output: 98


🏁 Script executed:

cat -n next.config.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 3202


🌐 Web query:

Next.js 15 RemotePattern type public API import

💡 Result:

Next.js 15 does not expose a public RemotePattern type you can import directly (e.g. there’s no import type { RemotePattern } from 'next').

Use the public NextConfig type and derive it from images.remotePatterns:

import type { NextConfig } from "next";

type RemotePattern =
  Exclude<NonNullable<NextConfig["images"]>["remotePatterns"][number], URL>;

Notes:

  • remotePatterns is configured under images in next.config.*. [1]
  • Since Next.js 15.3.0, remotePatterns also supports URL objects, which is why the Exclude<..., URL> is useful if you only want the object-shape pattern type. [2]

Sources: [1] [2]


🌐 Web query:

Next.js next/dist/shared/lib/image-config internal API alternative

💡 Result:

next/dist/shared/lib/image-config is not a public API (it can change/break across Next.js versions). The supported alternatives depend on what you were using it for:

  1. If you need the image config values (deviceSizes, imageSizes, formats, qualities, etc.)
  • Use the public images options in next.config.js and treat them as the source of truth. Next.js documents the allowed shape and the defaults (e.g., default deviceSizes, imageSizes, formats, etc.). [1] [2]
  1. If you need the computed <img> props / srcSet generation
  • Use the public getImageProps API from next/image instead of reimplementing logic using internal config modules. [3]
  1. If you were importing defaults (e.g., “imageConfigDefault”)
  • There is no supported “import defaults” API; copy the defaults from the docs into your own constants (or explicitly set them in next.config.js so they’re stable for your app). The docs list the defaults for deviceSizes, imageSizes, and qualities (and note qualities becomes required starting in Next.js 16). [3]

Sources: Next.js images config reference and validation shape [2] [1], next/image docs including defaults + getImageProps [3].


next/dist/shared/lib/image-config은 내부 API이므로 공개 타입을 사용하세요

next/dist/shared/lib/image-config는 Next.js의 공개 API가 아니어서 향후 버전에서 변경될 수 있습니다. NextConfig 타입으로부터 RemotePattern 타입을 유도하여 사용하세요:

import type { NextConfig } from 'next';

type RemotePattern =
  Exclude<NonNullable<NextConfig["images"]>["remotePatterns"][number], URL>;

그리고 localhost:1337 패턴(47-52줄)도 localhost:8080처럼 isProd 조건으로 감싸는 것을 검토하세요.

🤖 Prompt for AI Agents
In `@next.config.ts` at line 2, Replace the internal import of RemotePattern from
'next/dist/shared/lib/image-config' by deriving RemotePattern from the public
NextConfig type (use NextConfig and compute RemotePattern as
Exclude<NonNullable<NextConfig["images"]>["remotePatterns"][number], URL>) and
update any type annotations that reference RemotePattern in next.config.ts to
use this derived type; also review the remotePatterns entries and wrap the
localhost:1337 pattern (the block around the remotePatterns entries currently
including localhost:1337, lines ~47-52) with the same isProd conditional used
for localhost:8080 so local-only patterns are guarded by the environment check.


const isProd = process.env.NODE_ENV === 'production';

const nextConfig: NextConfig = {
// output: 'standalone',
Expand Down Expand Up @@ -31,7 +34,16 @@ const nextConfig: NextConfig = {
hostname: 'www.zeroone.it.kr',
pathname: '/**',
},
// CMS 개발 환경에서 사용하는 이미지 도메인 허용 설정
...(isProd
? ([] as RemotePattern[])
: ([
{
protocol: 'http',
hostname: 'localhost',
port: '8080',
pathname: '/**',
},
] as RemotePattern[])),
{
protocol: 'http',
hostname: 'localhost',
Expand Down
10 changes: 8 additions & 2 deletions src/app/(service)/home/home-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ import CommunityTab from '@/features/study/one-to-one/balance-game/ui/community-
import HallOfFameTab from '@/features/study/one-to-one/hall-of-fame/ui/hall-of-fame-tab';
import StudyHistoryTab from '@/features/study/one-to-one/history/ui/study-history-tab';
import StudyTab from '@/features/study/one-to-one/schedule/ui/home-study-tab';
import { getServerCookie } from '@/utils/server-cookie';
import { isNumeric } from '@/utils/validation';

interface HomeContentProps {
activeTab: string;
}

export default function HomeContent({ activeTab }: HomeContentProps) {
export default async function HomeContent({ activeTab }: HomeContentProps) {
const isHistoryTab = activeTab === 'history';
const memberIdStr = isHistoryTab ? await getServerCookie('memberId') : null;
const isLoggedIn = !!memberIdStr && isNumeric(memberIdStr);

const renderTabContent = () => {
switch (activeTab) {
case 'study':
return <StudyTab />;
case 'history':
return <StudyHistoryTab />;
return isLoggedIn ? <StudyHistoryTab /> : <StudyTab />;
case 'ranking':
return <HallOfFameTab />;
case 'archive':
Expand Down
6 changes: 3 additions & 3 deletions src/app/(service)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ export default function ServiceLayout({
return (
<html lang="ko">
<head>{GTM_ID && <GoogleTagManager gtmId={GTM_ID} />}</head>
<body className={clsx(pretendard.className, 'h-screen w-screen')}>
<body className={clsx(pretendard.className, 'min-h-screen w-screen')}>
<MainProvider>
<PageViewTracker />
<div className="flex h-screen w-full flex-col overflow-hidden">
<div className="flex min-h-screen w-full flex-col overflow-x-auto">
<Header />
<main className="w-full flex-1 overflow-auto">{children}</main>
<main className="w-full flex-1">{children}</main>
</div>
</MainProvider>
</body>
Expand Down
2 changes: 1 addition & 1 deletion src/components/card/voting-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function VotingCard({ voting, onClick }: VotingCardProps) {
</p>
)}

{/* 간단한 투표 결과 미리보기 (투표했을 때만) */}
{/* 간단한 투표 결과 미리보기 */}
{hasVoted && (
<div className="rounded-100 border-border-subtle bg-background-alternative mb-300 border p-300">
<div className="font-designer-12b text-text-subtle mb-100">
Expand Down
5 changes: 3 additions & 2 deletions src/components/filtering/study-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,16 @@ export default function StudyFilter({ values, onChange }: StudyFilterProps) {
type: [],
targetRoles: [],
method: [],
recruiting: false,
recruiting: true, // 기본값: 모집 중만 보기
});
}, [onChange]);

// recruiting은 기본값이므로 필터로 간주하지 않음
const hasAnyFilter =
values.type.length > 0 ||
values.targetRoles.length > 0 ||
values.method.length > 0 ||
values.recruiting;
!values.recruiting; // recruiting이 false면 필터 적용 중

return (
<div className="flex items-center gap-100">
Expand Down
10 changes: 9 additions & 1 deletion src/components/home/tab-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
History,
} from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { getCookie } from '@/api/client/cookie';
import { cn } from '@/components/ui/(shadcn)/lib/utils';
import { useAuth } from '@/hooks/common/use-auth';

interface TabNavigationProps {
activeTab: string;
Expand Down Expand Up @@ -50,6 +52,12 @@ const TABS = [
export default function TabNavigation({ activeTab }: TabNavigationProps) {
const router = useRouter();
const searchParams = useSearchParams();
const { isAuthenticated } = useAuth();
const hasMemberId = !!getCookie('memberId');
const canViewHistory = isAuthenticated && hasMemberId;
const visibleTabs = canViewHistory
? TABS
: TABS.filter((tab) => tab.id !== 'history');

const handleTabChange = (tabId: string) => {
const params = new URLSearchParams(searchParams.toString());
Expand All @@ -64,7 +72,7 @@ export default function TabNavigation({ activeTab }: TabNavigationProps) {
</div>

<nav className="border-border-subtle flex gap-100 border-b">
{TABS.map((tab) => {
{visibleTabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;

Expand Down
31 changes: 26 additions & 5 deletions src/components/pages/group-study-list-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useGetStudies } from '@/hooks/queries/study-query';
import GroupStudyFormModal from '../../features/study/group/ui/group-study-form-modal';
import GroupStudyPagination from '../../features/study/group/ui/group-study-pagination';
import GroupStudyList from '../lists/group-study-list';
import MyParticipatingStudiesSection from '../section/my-participating-studies-section';

// Carousel이 클라이언트 전용이므로 dynamic import로 로드
const Banner = dynamic(() => import('@/widgets/home/banner'), {
Expand All @@ -38,12 +39,18 @@ export default function GroupStudyListPage() {
const [searchQuery, setSearchQuery] = useState('');

// URL에서 필터 값 읽기
// 기본값: recruiting = true (모집 중만 보기)
// 사용자가 토글을 조작하면 URL에 명시적으로 저장됨
const filterValues = useMemo<StudyFilterValues>(() => {
const type = searchParams.get('type')?.split(',').filter(Boolean) ?? [];
const targetRoles =
searchParams.get('targetRoles')?.split(',').filter(Boolean) ?? [];
const method = searchParams.get('method')?.split(',').filter(Boolean) ?? [];
const recruiting = searchParams.get('recruiting') === 'true';
// URL에 recruiting 파라미터가 없으면 기본값 true (모집 중만 보기)
// 파라미터가 있으면 그 값 사용 (명시적 사용자 선택)
const recruitingParam = searchParams.get('recruiting');
const recruiting =
recruitingParam === null ? true : recruitingParam === 'true';

return { type, targetRoles, method, recruiting };
}, [searchParams]);
Expand All @@ -67,7 +74,8 @@ export default function GroupStudyListPage() {
filterValues.method.length > 0
? (filterValues.method as GetGroupStudiesMethodEnum[])
: undefined,
recruiting: filterValues.recruiting || undefined,
// 기본값: true (모집 중만), false면 전체 조회
recruiting: filterValues.recruiting ? true : undefined,
});

const allStudies = useMemo(() => data?.content ?? [], [data?.content]);
Expand All @@ -78,10 +86,16 @@ export default function GroupStudyListPage() {
const params = new URLSearchParams(searchParams.toString());

Object.entries(updates).forEach(([key, value]) => {
if (value === undefined || value === '' || value === 'false') {
if (value === undefined || value === '') {
params.delete(key);
} else {
params.set(key, value);
// recruiting의 경우 'false'도 명시적으로 저장 (기본값이 true이므로)
// 다른 필터는 'false'일 때 파라미터 제거
if (key === 'recruiting' || value !== 'false') {
params.set(key, value);
} else {
params.delete(key);
}
}
});

Expand All @@ -97,6 +111,8 @@ export default function GroupStudyListPage() {
);

// 필터 변경 핸들러
// recruiting 토글: true면 'true' 저장, false면 'false' 명시적으로 저장
// (기본값이 true이므로 false일 때도 명시적으로 저장해야 토글이 정상 작동)
const handleFilterChange = useCallback(
(values: StudyFilterValues) => {
updateSearchParams({
Expand All @@ -106,7 +122,9 @@ export default function GroupStudyListPage() {
? values.targetRoles.join(',')
: undefined,
method: values.method.length > 0 ? values.method.join(',') : undefined,
recruiting: values.recruiting ? 'true' : undefined,
// recruiting: true면 'true', false면 'false' 명시적으로 저장
// (기본값이 true이므로 false일 때도 URL에 저장해야 토글 해제 가능)
recruiting: values.recruiting ? 'true' : 'false',
});
},
[updateSearchParams],
Expand Down Expand Up @@ -158,6 +176,9 @@ export default function GroupStudyListPage() {
<Banner />
</div>

{/* 내가 참여중인 스터디 섹션 */}
<MyParticipatingStudiesSection classification="GROUP_STUDY" />

{/* 헤더 */}
<div className="mb-400 flex items-center justify-between">
<h1 className="font-designer-24b text-text-default">
Expand Down
Loading
Loading