Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6ef1591
fix : 참가자 항목 카운트 로직 수정
HA-SEUNG-JEONG Feb 17, 2026
c704aae
fix : 왕관 UI 수정, 남은 참가자 목록이 호버 시 열리도록 수정
HA-SEUNG-JEONG Feb 17, 2026
e185e96
fix : 스터디 상세 페이지 첫 렌더 시 신청하기 버튼 오표시 수정
HA-SEUNG-JEONG Feb 18, 2026
1586b9b
fix : 종료된 스터디에 대한 UI 처리 변경
HA-SEUNG-JEONG Feb 18, 2026
a256e55
Merge branch 'develop' of https://github.com/code-zero-to-one/study-p…
HA-SEUNG-JEONG Feb 18, 2026
2174eb6
fix : 네이밍 변경
HA-SEUNG-JEONG Feb 18, 2026
24221aa
fix : 그룹스터디 미신청자는 문의하기 버튼 숨김
HA-SEUNG-JEONG Feb 18, 2026
e5c8e2f
feat : 스터디 상세 페이지에 커리큘럼 요약 추가
HA-SEUNG-JEONG Feb 18, 2026
dec628d
feat : 최소 길이 추가
HA-SEUNG-JEONG Feb 19, 2026
9846154
fix : 오타 수정, studyType 추가
HA-SEUNG-JEONG Feb 19, 2026
6f23805
fix : null 타입 제거, 인터페이스 네이밍 변경
HA-SEUNG-JEONG Feb 19, 2026
fc3c72c
feat : 문의 목록, 문의 상세 조회 추가
HA-SEUNG-JEONG Feb 19, 2026
5e75338
fix : 토스트 뜨는 타이밍 수정
HA-SEUNG-JEONG Feb 19, 2026
55cb27b
feat : 문의 상세 및 문의 목록에 조회 수 추가
HA-SEUNG-JEONG Feb 20, 2026
1d1f46c
feat : 이미지 첨부 컴포넌트 추가 및 해당 컴포넌트 재사용하도록 수정
HA-SEUNG-JEONG Feb 20, 2026
caf8389
fix : alert를 toast로 대체
HA-SEUNG-JEONG Feb 20, 2026
15b9b47
feat : 스터디 카드 카운트다운 뱃지 및 상세 전광판 추가
HA-SEUNG-JEONG Feb 20, 2026
51f81a3
feat : 스터디 목록 경험 레벨 필터 추가
HA-SEUNG-JEONG Feb 20, 2026
f309c38
feat : 문의 API 타입 확장 (이미지, 조회수, 페이지네이션 필드 추가)
HA-SEUNG-JEONG Feb 20, 2026
b011867
style : 코드 포맷팅 수정
HA-SEUNG-JEONG Feb 20, 2026
bb513e6
fix : 팝오버 UI 수정
HA-SEUNG-JEONG Feb 20, 2026
dc1186b
delete : 주석 제거
HA-SEUNG-JEONG Feb 22, 2026
d15a718
fix : axios -> axiosV2로 부분 마이그레이션
HA-SEUNG-JEONG Feb 22, 2026
8121a25
feat: 문의 답변 API 타입 확장 및 useCreateAnswer 훅 추가
HA-SEUNG-JEONG Feb 25, 2026
9a698b5
feat: InquiryStatusBadge 컴포넌트 추가 및 문의 탭 섹션 분리
HA-SEUNG-JEONG Feb 25, 2026
ae6308b
feat: 문의 목록/상세 페이지 UI 개선 및 그룹 스터디 상세에 InquirySection 통합
HA-SEUNG-JEONG Feb 25, 2026
abdf09f
fix: DetailView UI 개선 및 불필요한 로그 제거
HA-SEUNG-JEONG Feb 25, 2026
7715ac9
fix: 문의 관련 UI 개선 및 날짜 포맷 함수 통합
HA-SEUNG-JEONG Feb 25, 2026
d5bb32f
fix: InquiryDetailPage UI 개선 및 답변 등록 모달 제거
HA-SEUNG-JEONG Feb 25, 2026
da0e58b
fix: Inquiry 관련 UI 개선 및 InquiryListTable 컴포넌트 추가
HA-SEUNG-JEONG Feb 25, 2026
ccf330f
fix: ListView에서 문의하기 버튼 제거
HA-SEUNG-JEONG Feb 25, 2026
664ca1f
fix: InquiryDetailPage에서 잘못된 접근 처리 메시지 추가 및 UI 개선
HA-SEUNG-JEONG Feb 25, 2026
f318a26
fix: 빈 줄 추가
HA-SEUNG-JEONG Feb 25, 2026
aa62da2
fix : 카테고리 텍스트 수정
HA-SEUNG-JEONG Feb 25, 2026
0382ced
fix: InquiryDetailPage에서 오류 처리 메시지 추가 및 상태 관리 개선
HA-SEUNG-JEONG Feb 25, 2026
d7de619
fix: Toast 컴포넌트에 정보 메시지 추가 및 관련 수정
HA-SEUNG-JEONG Feb 25, 2026
91b1ca9
fix: 날짜 문자열 유효성 검사 추가 및 포맷 함수 수정
HA-SEUNG-JEONG Feb 25, 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
2 changes: 0 additions & 2 deletions src/app/(service)/(my)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Metadata } from 'next';
import GlobalToast from '@/components/ui/global-toast';
import Sidebar from '@/widgets/my-page/sidebar';

export const metadata: Metadata = {
Expand All @@ -14,7 +13,6 @@ export default function MyLayout({
}>) {
return (
<div className="flex h-full">
<GlobalToast />
<Sidebar />
<div className="m-auto pt-500 pb-[100px]">
<div className="w-[780px]">{children}</div>
Expand Down
9 changes: 1 addition & 8 deletions src/app/(service)/group-study/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import GlobalToast from '@/components/ui/global-toast';

export default function GroupStudyLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<GlobalToast />
{children}
</>
);
return <>{children}</>;
}
2 changes: 0 additions & 2 deletions src/app/(service)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Metadata } from 'next';
import StartStudyButton from '@/components/home/start-study-button';
import GlobalToast from '@/components/ui/global-toast';
import { generateMetadata as generateSEOMetadata } from '@/utils/seo';
import Banner from '@/widgets/home/banner';
import FeedbackLink from '@/widgets/home/feedback-link';
Expand All @@ -25,7 +24,6 @@ export default async function Home({

return (
<div className="mx-auto flex w-[1496px] flex-col gap-500 px-600 py-600">
<GlobalToast />
<Banner />
<FeedbackLink />
<StartStudyButton />
Expand Down
181 changes: 181 additions & 0 deletions src/app/(service)/inquiry/[questionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
'use client';

import { ArrowLeft, Eye } from 'lucide-react';
import Image from 'next/image';
import { useRouter, useSearchParams } from 'next/navigation';
import { use } from 'react';
import InquiryStatusBadge from '@/components/ui/badge/inquiry-status-badge';
import MoreMenu from '@/components/ui/dropdown/more-menu';
import { CATEGORY_LABEL } from '@/features/study/group/model/question.schema';
import { useGetQuestion } from '@/hooks/queries/question-api';
import { useToastStore } from '@/stores/use-toast-store';
import { formatDateTimeDot } from '@/utils/time';

export default function InquiryDetailPage({
params,
}: {
params: Promise<{ questionId: string }>;
}) {
const { questionId: questionIdStr } = use(params);
const router = useRouter();
const searchParams = useSearchParams();
const groupStudyIdStr = searchParams.get('groupStudyId');
const groupStudyId = groupStudyIdStr ? Number(groupStudyIdStr) : 0;
const studyType = searchParams.get('studyType') ?? 'group';
const questionId = Number(questionIdStr);
const showToast = useToastStore((state) => state.showToast);

const { data, isLoading, isError } = useGetQuestion({
groupStudyId,
questionId,
});

const handleBack = () => {
router.push(`/inquiry?groupStudyId=${groupStudyId}&studyType=${studyType}`);
};

const moreMenuOptions = [
{
label: '수정하기',
value: 'edit',
onMenuClick: () => showToast('준비 중인 기능입니다.', 'info'),
},
{
label: '삭제하기',
value: 'delete',
onMenuClick: () => showToast('준비 중인 기능입니다.', 'info'),
},
];

if (!groupStudyId) {
return (
<div className="mx-auto w-full max-w-7xl px-400 py-600">
<div className="text-text-subtle py-800 text-center">
잘못된 접근입니다. 스터디 문의 목록에서 다시 접근해주세요.
</div>
</div>
);
}

if (isLoading) {
return (
<div className="mx-auto w-full max-w-7xl px-400 py-600">
<div className="text-text-subtle py-800 text-center">로딩 중...</div>
</div>
);
}

if (isError || (!isLoading && !data)) {
return (
<div className="mx-auto w-full max-w-7xl px-400 py-600">
<div className="text-text-subtle py-800 text-center">
문의를 불러오는 중 오류가 발생했습니다. 다시 시도해주세요.
</div>
</div>
);
}

return (
<div className="mx-auto w-full max-w-7xl px-400 py-600">
<div className="mb-400">
<button
onClick={handleBack}
className="text-text-subtle hover:text-text-default font-designer-14r flex items-center gap-100 transition-colors"
>
<ArrowLeft size={16} />
목록으로
</button>
</div>

{data && (
<div className="border-border-default rounded-100 border">
{/* 문의 헤더 */}
<div className="px-600 py-400">
<div className="mb-200 flex items-start justify-between">
<div className="flex flex-col gap-200">
{data.category && (
<span className="bg-background-accent-gray-subtle text-background-accent-gray-strong font-designer-12m rounded-50 inline-flex w-fit px-100 py-50">
{CATEGORY_LABEL[data.category] ?? data.category}
</span>
)}
<h1 className="font-designer-24b text-text-strong">
{data.title}
</h1>
</div>
<MoreMenu options={moreMenuOptions} iconSize={20} />
</div>

<div className="font-designer-13r text-text-subtle border-border-default grid grid-cols-2 gap-y-100 border-b pb-300">
<div className="flex gap-200">
<span className="text-text-subtle w-[60px]">작성자</span>
<span className="text-text-default">{data.authorNickname}</span>
</div>
<div className="flex items-center gap-200">
<Eye size={14} className="text-text-subtle" />
<span className="text-text-default">{data.viewCount}</span>
</div>
<div className="flex gap-200">
<span className="text-text-subtle w-[60px]">작성일</span>
<span className="text-text-default">
{formatDateTimeDot(data.createdAt)}
</span>
</div>
<div className="flex items-center gap-200">
<InquiryStatusBadge status={data.status} />
</div>
</div>
</div>

{/* 구분선 */}
<div className="px-600">
<hr className="border-border-default" />
</div>

{/* 문의 내용 */}
<div className="px-600 py-400">
<p className="font-designer-16r text-text-default whitespace-pre-wrap">
{data.content}
</p>
{data.questionImage?.resizedImages?.[0]?.resizedImageUrl && (
<Image
src={data.questionImage.resizedImages[0].resizedImageUrl}
alt="문의 이미지"
width={800}
height={600}
className="mt-400 w-full object-contain"
style={{ height: 'auto' }}
/>
)}
</div>

{/* 구분선 */}
<div className="px-600">
<hr className="border-border-default" />
</div>

{/* 답변 섹션 */}
<div className="px-600 py-400">
<h2 className="font-designer-16b text-text-strong mb-300">답변</h2>
{data.answer ? (
<div className="flex flex-col gap-200">
<div className="font-designer-13r text-text-subtle flex items-center gap-200">
<span>{data.answererNickname}</span>
<span>{formatDateTimeDot(data.answeredAt ?? '')}</span>
</div>
<p className="font-designer-14r text-text-default whitespace-pre-wrap">
{data.answer}
</p>
</div>
) : (
<div className="border-border-default rounded-200 flex items-center justify-center border bg-white py-500">
<p className="font-designer-14r text-text-subtle">
아직 답변이 등록되지 않았습니다.
</p>
</div>
)}
</div>
</div>
)}
</div>
);
}
93 changes: 93 additions & 0 deletions src/app/(service)/inquiry/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client';

import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import InquiryListTable from '@/components/lists/inquiry-list-table';
import QuestionModal from '@/components/modals/question-modal';
import Button from '@/components/ui/button';
import { useGetQuestions } from '@/hooks/queries/question-api';

const PAGE_SIZE = 15;

export default function InquiryPage() {
const router = useRouter();
const searchParams = useSearchParams();
const groupStudyIdStr = searchParams.get('groupStudyId');
const groupStudyId = groupStudyIdStr ? Number(groupStudyIdStr) : null;
const studyType = (searchParams.get('studyType') ?? 'group') as
| 'group'
| 'premium';
const isPremium = studyType === 'premium';

const [page, setPage] = useState(1);
const [isModalOpen, setIsModalOpen] = useState(false);

useEffect(() => {
if (!groupStudyId) {
router.replace('/group-study');
}
}, [groupStudyId, router]);

const handleItemClick = (questionId: number) => {
router.push(
`/inquiry/${questionId}?groupStudyId=${groupStudyId}&studyType=${studyType}`,
);
};

const { data, isLoading } = useGetQuestions({
groupStudyId: groupStudyId ?? 0,
page,
pageSize: PAGE_SIZE,
});

if (!groupStudyId) return null;

const items = data?.content ?? [];
const totalPages = data?.totalPages ?? 1;
const totalElements = data?.totalElements ?? 0;

return (
<div className="mx-auto w-full max-w-7xl px-400 py-600">
{/* 헤더 */}
<div className="mb-400 flex items-start justify-between">
<div className="flex flex-col gap-75">
<h1 className="font-designer-24b text-text-strong">
문의 게시판{' '}
<span className="font-designer-20b text-text-subtle">
{totalElements}개
</span>
</h1>
<p className="font-designer-14r text-text-subtle">
스터디 관련 문의사항을 남겨주세요
</p>
<p className="font-designer-14r text-text-subtle">
비공개 문의는 작성자, {isPremium ? '멘토' : '리더'}, 관리자만 확인할
수 있어요.
</p>
</div>
<Button color="primary" onClick={() => setIsModalOpen(true)}>
문의하기
</Button>
</div>

{/* 표 */}
<InquiryListTable
items={items}
totalElements={totalElements}
totalPages={totalPages}
page={page}
isLoading={isLoading}
onPageChange={setPage}
onItemClick={(item) => handleItemClick(item.questionId)}
/>

{/* 문의하기 모달 */}
<QuestionModal
open={isModalOpen}
onOpenChange={setIsModalOpen}
studyId={groupStudyId}
studyType={studyType}
/>
</div>
);
}
4 changes: 4 additions & 0 deletions src/app/(service)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import localFont from 'next/font/local';
import React from 'react';
import ClarityInit from '@/components/analytics/clarity-init';
import PageViewTracker from '@/components/analytics/page-view-tracker';
import FloatingInquiryButton from '@/components/ui/floating-inquiry-button';
import GlobalToast from '@/components/ui/global-toast';
import MainProvider from '@/providers';
import { getServerCookie } from '@/utils/server-cookie';
import Header from '@/widgets/home/header';
Expand Down Expand Up @@ -40,11 +42,13 @@ export default async function ServiceLayout({
<head>{GTM_ID && <GoogleTagManager gtmId={GTM_ID} />}</head>
<body className={clsx(pretendard.className, 'min-h-screen w-screen')}>
<MainProvider initialAccessToken={initialAccessToken ?? undefined}>
<GlobalToast />
<ClarityInit projectId={CLARITY_PROJECT_ID} />
<PageViewTracker />
<div className="flex min-h-screen w-full flex-col overflow-x-auto">
<Header />
<main className="w-full flex-1">{children}</main>
<FloatingInquiryButton />
</div>
</MainProvider>
</body>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(service)/premium-study/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default async function Page({
if (!isLeader && memberId) {
// 내가 리더가 아닐 경우에만 내 신청 상태 정보 미리 가져오기
await queryClient.prefetchQuery({
queryKey: ['groupStudyMyStatus', Number(id)],
queryKey: ['groupStudyMemberStatus', Number(id)],
queryFn: () =>
getGroupStudyMyStatusInServer({ groupStudyId: Number(id) }),
});
Expand Down
9 changes: 1 addition & 8 deletions src/app/(service)/premium-study/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import GlobalToast from '@/components/ui/global-toast';

export default function PremiumStudyLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<GlobalToast />
{children}
</>
);
return <>{children}</>;
}
Loading
Loading