From d84f268e25a8c0ea4765065bad48d1ffe71b06f7 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 20 Dec 2025 23:17:18 +0900 Subject: [PATCH 001/211] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/app/(service)/payment/[id]/page.tsx | 102 ++++++++++ src/app/(service)/payment/complete/page.tsx | 125 ++++++++++++ src/app/(service)/study/page.tsx | 18 ++ src/components/payment/orderSummary.tsx | 27 +++ .../payment/paymentActionClient.tsx | 183 ++++++++++++++++++ src/components/payment/priceSummary.tsx | 24 +++ src/components/ui/radio/index.tsx | 4 +- .../study/group/ui/group-study-list.tsx | 4 +- yarn.lock | 5 + 10 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 src/app/(service)/payment/[id]/page.tsx create mode 100644 src/app/(service)/payment/complete/page.tsx create mode 100644 src/components/payment/orderSummary.tsx create mode 100644 src/components/payment/paymentActionClient.tsx create mode 100644 src/components/payment/priceSummary.tsx diff --git a/package.json b/package.json index 3e61c778..3aa4020e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.0.6", + "@tosspayments/tosspayments-sdk": "^2.5.0", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/app/(service)/payment/[id]/page.tsx b/src/app/(service)/payment/[id]/page.tsx new file mode 100644 index 00000000..a51a8d69 --- /dev/null +++ b/src/app/(service)/payment/[id]/page.tsx @@ -0,0 +1,102 @@ +// app/checkout/page.tsx + +import OrderSummary from '@/components/payment/orderSummary'; +import PaymentCheckoutPage from '@/components/payment/paymentActionClient'; +import CheckoutActionClient from '@/components/payment/paymentActionClient'; +import PriceSummary from '@/components/payment/priceSummary'; + +interface Study { + id: string; + title: string; + desc: string; + price: number; + thumbnailUrl?: string; +} + +interface Terms { + id: string; + label: string; + required: boolean; + url?: string; +} + +interface PaymentMethod { + id: 'CARD' | 'VBANK'; + label: string; + subLabel?: string; +} + +async function getCheckoutData(): Promise<{ + study: Study; + terms: Terms[]; + methods: PaymentMethod[]; +}> { + // 실제론 db / internal api에서 가져오기 + return { + study: { + id: 'study_1', + title: '1일1코테류프를 인증 챌린지', + desc: '1인 개발자, 이제는 대세가 되었다죠.', + price: 35000, + thumbnailUrl: '', + }, + terms: [ + { + id: 'terms_usage', + label: '이용약관 동의 (필수)', + required: true, + url: '/terms/usage', + }, + ], + methods: [ + { id: 'CARD', label: '신용카드 결제' }, + { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, + ], + }; +} + +export default async function CheckoutPage() { + const { study, terms, methods } = await getCheckoutData(); + + // const clientKey = 'test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm'; + // const tossPayments = await loadTossPayments(clientKey); + + // const customerKey = "pfROxUh11lHWaPrBxzIBN"; + // const widgets = tossPayments.widgets({ + // customerKey, + // }); + + return ( +
+
+
+ {/* 서버 렌더: 선택한 스터디 */} +
+

선택한 스터디

+ + +
+ + {/* 서버 렌더: 결제 금액 */} +
+

결제 금액

+ +
+ +
+
+ + {/* 클라 렌더: 약관/결제수단/결제하기 */} +
+ +
+
+
+
+ ); +} diff --git a/src/app/(service)/payment/complete/page.tsx b/src/app/(service)/payment/complete/page.tsx new file mode 100644 index 00000000..2d07c9ed --- /dev/null +++ b/src/app/(service)/payment/complete/page.tsx @@ -0,0 +1,125 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import Button from '@/components/ui/button'; + +interface PaymentResult { + orderName: string; + productAmount: number; + paymentMethod: string; + totalAmount: number; +} + +// ✅ 가데이터 +const MOCK_PAYMENT_RESULT: PaymentResult = { + orderName: '1일코테문풀 인증 챌린지', + productAmount: 35000, + paymentMethod: '카드결제', + totalAmount: 35000, +}; + +export default function PaymentCompletePage() { + const router = useRouter(); + const data = MOCK_PAYMENT_RESULT; + + const formatKRW = (n: number) => `${n.toLocaleString('ko-KR')}원`; + + return ( +
+
+ {/* 아이콘 */} +
+
+ success +
+
+ + {/* 타이틀 */} +
+

+ 스터디 수강 신청이 완료되었습니다. +

+

+ 수강/학습 내역과 결제 내역은 마이페이지에서 확인하실 수 있습니다. +

+
+ + {/* 정보 카드 */} +
+
+ + +
결제 정보
+ + + +
+ + + + +
+ + {/* 버튼 */} +
+ + + +
+
+
+
+ ); +} + +function Row({ + label, + value, + bold, + strong, +}: { + label: string; + value: string; + bold?: boolean; + strong?: boolean; +}) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx index 7e4bdd06..bc3bc409 100644 --- a/src/app/(service)/study/page.tsx +++ b/src/app/(service)/study/page.tsx @@ -1,3 +1,6 @@ +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import { Configuration } from '@/api/openapi/configuration'; +import { GroupStudyFullResponseDto } from '@/api/openapi/models'; import Button from '@/components/ui/button'; import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; import GroupStudyList from '@/features/study/group/ui/group-study-list'; @@ -5,10 +8,25 @@ import IconPlus from '@/shared/icons/plus.svg'; import { getServerCookie } from '@/utils/server-cookie'; import Sidebar from '@/widgets/home/sidebar'; +interface GroupStudyResponse { + content?: GroupStudyFullResponseDto; +} + export default async function Study() { const memberIdStr = await getServerCookie('memberId'); const isLoggedIn = !!memberIdStr; + // const config = new Configuration({ + // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, + // }); + + // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); + + // const response = await groupStudyApi.getGroupStudies(); + // const groupStudy = (response.data as GroupStudyResponse)?.content; + + // console.log('groupStudy', groupStudy); + return (
diff --git a/src/components/payment/orderSummary.tsx b/src/components/payment/orderSummary.tsx new file mode 100644 index 00000000..4a9cf04f --- /dev/null +++ b/src/components/payment/orderSummary.tsx @@ -0,0 +1,27 @@ +interface Props { + study: { + title: string; + desc: string; + price: number; + thumbnailUrl?: string; + }; +} + +export default function OrderSummary({ study }: Props) { + return ( +
+
+
+
+
+

{study.title}

+

{study.desc}

+
+

+ {study.price.toLocaleString()}원 +

+
+
+
+ ); +} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx new file mode 100644 index 00000000..77a77253 --- /dev/null +++ b/src/components/payment/paymentActionClient.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; +import { useEffect, useMemo, useState } from 'react'; +import { cn } from '../ui/(shadcn)/lib/utils'; +import Button from '../ui/button'; +import Checkbox from '../ui/checkbox'; +import { RadioGroup, RadioGroupItem } from '../ui/radio'; + +interface Props { + orderId: string; + amount: number; +} + +type PaymentMethod = 'CARD' | 'VBANK'; +const clientKey = 'test_ck_ORzdMaqN3wEbO04g0xNNr5AkYXQG'; +const customerKey = 'TwG7MSXwcuFlMaug2sHpf'; + +const methods: { id: PaymentMethod; label: string }[] = [ + { id: 'CARD', label: '신용카드 결제' }, + { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, +]; + +export default function PaymentCheckoutPage({ + orderId, + // amount, +}: Props) { + const [payment, setPayment] = useState(null); + const [isAgreed, setIsAgreed] = useState(false); + + const [errorMsg, setErrorMsg] = useState(null); + + const [paymentMethod, setPaymentMethod] = useState('CARD'); + + const canPay = isAgreed && !!paymentMethod; + + const toggleTerm = () => { + setIsAgreed((prev) => !prev); + }; + + const onPay = async () => { + setErrorMsg(null); + + if (!isAgreed) { + setErrorMsg('필수 약관에 동의해 주세요.'); + + return; + } + + if (!paymentMethod) { + setErrorMsg('결제 수단을 선택해 주세요.'); + + return; + } + + try { + await payment.requestPayment({ + method: 'CARD', // 카드 및 간편결제 + amount: 50000, // 결제 금액 + orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 + orderName: '토스 티셔츠 외 2건', + successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL + failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + customerEmail: 'customer123@gmail.com', + customerName: '김토스', + customerMobilePhone: '01012341234', + // 카드 결제에 필요한 정보 + card: { + useEscrow: false, + flowMode: 'DEFAULT', // 통합결제창 여는 옵션 + useCardPoint: false, + useAppCardOnly: false, + }, + }); + + // if (!res.ok) throw new Error('PAYMENT_SESSION_FAILED'); + + // const data: { redirectUrl: string } = await res.json(); + // window.location.href = data.redirectUrl; + } catch { + setErrorMsg('결제 요청에 실패했어요. 잠시 후 다시 시도해 주세요.'); + } finally { + } + }; + + const [amount] = useState({ + currency: 'KRW', + value: 50000, + }); + + useEffect(() => { + async function fetchPayment() { + try { + const tossPayments = await loadTossPayments(clientKey); + + // 회원 결제 + // @docs https://docs.tosspayments.com/sdk/v2/js#tosspaymentspayment + const payment = tossPayments.payment({ + customerKey, + }); + + setPayment(payment); + } catch (error) { + console.error('Error fetching payment:', error); + } + } + + fetchPayment().catch((error) => { + console.error('Error in fetchPayment:', error); + }); + }, [clientKey, customerKey]); + + return ( +
+ {/* 이용약관 */} +
+
+
+ + + 이용약관 동의 (필수) + +
+ + + 내용보기 + +
+
+ + {/* 결제수단 */} +
+

결제 수단

+ + setPaymentMethod(v as PaymentMethod)} + className="space-y-200" + > + {methods.map((m) => { + const selected = paymentMethod === m.id; + + return ( + + ); + })} + + + +
+
+ ); +} diff --git a/src/components/payment/priceSummary.tsx b/src/components/payment/priceSummary.tsx new file mode 100644 index 00000000..07c32572 --- /dev/null +++ b/src/components/payment/priceSummary.tsx @@ -0,0 +1,24 @@ +// app/checkout/_components/PriceSummary.tsx +interface Props { + price: number; +} + +export default function PriceSummary({ price }: Props) { + return ( +
+
+

상품 금액

+

+ {price.toLocaleString()}원 +

+
+ +
+ +
+

총 결제 금액

+

{price.toLocaleString()}원

+
+
+ ); +} diff --git a/src/components/ui/radio/index.tsx b/src/components/ui/radio/index.tsx index 9e45c113..d0eeb133 100644 --- a/src/components/ui/radio/index.tsx +++ b/src/components/ui/radio/index.tsx @@ -19,7 +19,7 @@ function RadioGroup({ } const radioGroupItemVariants = cva( - 'border-border-default hover:bg-fill-neutral-subtle-hover disabled:border-border-disabled disabled:bg-icon-disabled data-[state=checked]:border-border-success rounded-full border-2 bg-white data-[state=checked]:border-4', + 'border-border-default hover:bg-fill-neutral-subtle-hover disabled:border-border-disabled disabled:bg-icon-disabled data-[state=checked]:border-border-success rounded-full border-2 bg-[#fff] data-[state=checked]:border-4', { variants: { size: { @@ -45,7 +45,7 @@ function RadioGroupItem({ className={cn(radioGroupItemVariants({ size }), className)} {...props} > - + ); } diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 846cd85d..6f355e81 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -149,7 +149,7 @@ export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { ))}
- + /> */}
); })} diff --git a/yarn.lock b/yarn.lock index 33d39834..c93995c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2838,6 +2838,11 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== +"@tosspayments/tosspayments-sdk@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@tosspayments/tosspayments-sdk/-/tosspayments-sdk-2.5.0.tgz#2ebc019be3db092e6603f1ba42edf3d9655b183a" + integrity sha512-qapms+cTY5/4MBwcEegG1GkYBIWVjWv77yWnG2+CWXqdZOtGKLmqt0pb1lbrevBOF/uhO1f2Wtj+r/bggHwiHQ== + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" From a71852c29bf1d2ed34d5a5a71850d71f5f0fb541 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 27 Dec 2025 00:04:40 +0900 Subject: [PATCH 002/211] =?UTF-8?q?=EC=9C=A0=EB=A3=8C=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client/open-api-instance.ts | 10 + src/app/(service)/(my)/my-study/page.tsx | 2 + .../{study => group-study}/[id]/page.tsx | 0 src/app/(service)/group-study/page.tsx | 65 ++++ src/app/(service)/payment/[id]/page.tsx | 67 +--- src/app/(service)/payment/fail/page.tsx | 99 ++++++ src/app/(service)/payment/success/page.tsx | 171 +++++++++ src/app/(service)/premium-study/[id]/page.tsx | 112 ++++++ src/app/(service)/premium-study/page.tsx | 65 ++++ src/app/(service)/study/page.tsx | 56 --- src/components/payment/PaymentTermsModal.tsx | 116 +++++++ .../payment/paymentActionClient.tsx | 71 ++-- .../my-page/ui/my-study-info-card.tsx | 2 +- .../group/api/get-group-study-list.server.ts | 26 ++ .../study/group/api/get-group-study-list.ts | 3 +- .../group/model/group-study-form.schema.ts | 17 +- .../group/model/use-group-study-list-query.ts | 1 + .../study/group/ui/group-study-form-modal.tsx | 8 +- .../study/group/ui/group-study-form.tsx | 14 +- .../study/group/ui/group-study-list.tsx | 300 +++++++--------- .../study/group/ui/group-study-pagination.tsx | 32 ++ .../study/group/ui/step/step1-group.tsx | 38 +- .../premium/ui/premium-study-detail-page.tsx | 238 +++++++++++++ .../premium/ui/premium-study-info-section.tsx | 326 ++++++++++++++++++ .../study/premium/ui/premium-study-list.tsx | 159 +++++++++ .../premium/ui/premium-study-pagination.tsx | 32 ++ .../premium/ui/premium-summary-study-info.tsx | 144 ++++++++ src/widgets/home/header.tsx | 4 +- 28 files changed, 1837 insertions(+), 341 deletions(-) rename src/app/(service)/{study => group-study}/[id]/page.tsx (100%) create mode 100644 src/app/(service)/group-study/page.tsx create mode 100644 src/app/(service)/payment/fail/page.tsx create mode 100644 src/app/(service)/payment/success/page.tsx create mode 100644 src/app/(service)/premium-study/[id]/page.tsx create mode 100644 src/app/(service)/premium-study/page.tsx delete mode 100644 src/app/(service)/study/page.tsx create mode 100644 src/components/payment/PaymentTermsModal.tsx create mode 100644 src/features/study/group/api/get-group-study-list.server.ts create mode 100644 src/features/study/group/ui/group-study-pagination.tsx create mode 100644 src/features/study/premium/ui/premium-study-detail-page.tsx create mode 100644 src/features/study/premium/ui/premium-study-info-section.tsx create mode 100644 src/features/study/premium/ui/premium-study-list.tsx create mode 100644 src/features/study/premium/ui/premium-study-pagination.tsx create mode 100644 src/features/study/premium/ui/premium-summary-study-info.tsx diff --git a/src/api/client/open-api-instance.ts b/src/api/client/open-api-instance.ts index 644042af..04bae77b 100644 --- a/src/api/client/open-api-instance.ts +++ b/src/api/client/open-api-instance.ts @@ -22,3 +22,13 @@ export const createApiInstance = ( ): T => { return new ApiClass(openapiConfig, openapiConfig.basePath, axiosInstanceV2); }; + +export const createApiServerInstance = ( + ApiClass: new ( + config: Configuration, + basePath?: string, + axios?: AxiosInstance, + ) => T, +): T => { + return new ApiClass(openapiConfig, openapiConfig.basePath, axiosInstanceV2); +}; diff --git a/src/app/(service)/(my)/my-study/page.tsx b/src/app/(service)/(my)/my-study/page.tsx index d728059d..3cd11c4f 100644 --- a/src/app/(service)/(my)/my-study/page.tsx +++ b/src/app/(service)/(my)/my-study/page.tsx @@ -17,6 +17,8 @@ interface MemberGroupStudyList extends MemberStudyItem { export default function MyStudy() { const { data: authData } = useAuth(); + console.log('data', authData); + const { data, isLoading } = useMemberStudyListQuery({ memberId: authData?.memberId, studyType: 'GROUP_STUDY', diff --git a/src/app/(service)/study/[id]/page.tsx b/src/app/(service)/group-study/[id]/page.tsx similarity index 100% rename from src/app/(service)/study/[id]/page.tsx rename to src/app/(service)/group-study/[id]/page.tsx diff --git a/src/app/(service)/group-study/page.tsx b/src/app/(service)/group-study/page.tsx new file mode 100644 index 00000000..3198ea93 --- /dev/null +++ b/src/app/(service)/group-study/page.tsx @@ -0,0 +1,65 @@ +import { Play, Plus } from 'lucide-react'; +import { createApiServerInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import Button from '@/components/ui/button'; +import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; +import GroupStudyList from '@/features/study/group/ui/group-study-list'; +import GroupStudyPagination from '@/features/study/group/ui/group-study-pagination'; + +interface GroupStudyPageProps { + searchParams: Promise<{ page?: string }>; +} + +export default async function GroupStudyPage({ + searchParams, +}: GroupStudyPageProps) { + const params = await searchParams; + const currentPage = Number(params.page) || 1; + const pageSize = 9; + + const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudies( + 'GROUP_STUDY', + currentPage, + pageSize, + ); + + console.log('data', data); + + return ( +
+ {/* 헤더 */} +
+

+ 그룹스터디 둘러보기 +

+ } + iconPosition="left" + > + 스터디 개설하기 + + } + /> +
+ + {/* 스터디 카드 그리드 */} + + + {/* 페이지네이션 */} + {data.totalPages > 1 && ( + + )} +
+ ); +} diff --git a/src/app/(service)/payment/[id]/page.tsx b/src/app/(service)/payment/[id]/page.tsx index a51a8d69..1f4a76a2 100644 --- a/src/app/(service)/payment/[id]/page.tsx +++ b/src/app/(service)/payment/[id]/page.tsx @@ -2,8 +2,8 @@ import OrderSummary from '@/components/payment/orderSummary'; import PaymentCheckoutPage from '@/components/payment/paymentActionClient'; -import CheckoutActionClient from '@/components/payment/paymentActionClient'; import PriceSummary from '@/components/payment/priceSummary'; +import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; interface Study { id: string; @@ -13,58 +13,26 @@ interface Study { thumbnailUrl?: string; } -interface Terms { - id: string; - label: string; - required: boolean; - url?: string; +interface PaymentPageProps { + params: Promise<{ id: string }>; } -interface PaymentMethod { - id: 'CARD' | 'VBANK'; - label: string; - subLabel?: string; -} +async function getStudyData(groupStudyId: number): Promise { + const studyDetail = await getGroupStudyDetailInServer({ groupStudyId }); -async function getCheckoutData(): Promise<{ - study: Study; - terms: Terms[]; - methods: PaymentMethod[]; -}> { - // 실제론 db / internal api에서 가져오기 return { - study: { - id: 'study_1', - title: '1일1코테류프를 인증 챌린지', - desc: '1인 개발자, 이제는 대세가 되었다죠.', - price: 35000, - thumbnailUrl: '', - }, - terms: [ - { - id: 'terms_usage', - label: '이용약관 동의 (필수)', - required: true, - url: '/terms/usage', - }, - ], - methods: [ - { id: 'CARD', label: '신용카드 결제' }, - { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, - ], + id: String(studyDetail.basicInfo.groupStudyId), + title: studyDetail.detailInfo.title, + desc: studyDetail.detailInfo.summary, + price: studyDetail.basicInfo.price, + thumbnailUrl: + studyDetail.detailInfo.image?.resizedImages?.[0]?.resizedImageUrl || '', }; } -export default async function CheckoutPage() { - const { study, terms, methods } = await getCheckoutData(); - - // const clientKey = 'test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm'; - // const tossPayments = await loadTossPayments(clientKey); - - // const customerKey = "pfROxUh11lHWaPrBxzIBN"; - // const widgets = tossPayments.widgets({ - // customerKey, - // }); +export default async function CheckoutPage({ params }: PaymentPageProps) { + const { id } = await params; + const study = await getStudyData(Number(id)); return (
@@ -88,12 +56,7 @@ export default async function CheckoutPage() { {/* 클라 렌더: 약관/결제수단/결제하기 */}
- +
diff --git a/src/app/(service)/payment/fail/page.tsx b/src/app/(service)/payment/fail/page.tsx new file mode 100644 index 00000000..2fa5c423 --- /dev/null +++ b/src/app/(service)/payment/fail/page.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { useSearchParams, useRouter } from 'next/navigation'; +import Button from '@/components/ui/button'; + +// 토스페이먼츠 에러 코드별 사용자 친화적 메시지 +const ERROR_MESSAGES: Record = { + PAY_PROCESS_CANCELED: '결제가 취소되었습니다.', + PAY_PROCESS_ABORTED: '결제가 중단되었습니다.', + REJECT_CARD_COMPANY: '카드사에서 결제를 거절했습니다.', + INVALID_CARD_EXPIRATION: '카드 유효기간이 만료되었습니다.', + INVALID_STOPPED_CARD: '정지된 카드입니다.', + INVALID_CARD_LOST: '분실 신고된 카드입니다.', + INVALID_CARD_NUMBER: '카드 번호가 올바르지 않습니다.', + EXCEED_MAX_DAILY_PAYMENT_COUNT: '일일 결제 한도를 초과했습니다.', + EXCEED_MAX_PAYMENT_AMOUNT: '결제 금액 한도를 초과했습니다.', + NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT: + '할부가 지원되지 않는 카드입니다.', + INVALID_CARD_INSTALLMENT_PLAN: '할부 개월 수가 올바르지 않습니다.', + NOT_ALLOWED_POINT_USE: '포인트 사용이 불가한 카드입니다.', + INVALID_API_KEY: '결제 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + INVALID_ORDER_ID: '주문 정보가 올바르지 않습니다.', + INVALID_AMOUNT: '결제 금액이 올바르지 않습니다.', +}; + +function getErrorMessage(code: string | null, message: string | null): string { + if (code && ERROR_MESSAGES[code]) { + return ERROR_MESSAGES[code]; + } + + if (message) { + return message; + } + + return '결제 중 오류가 발생했습니다. 다시 시도해주세요.'; +} + +export default function PaymentFailPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const code = searchParams.get('code'); + const message = searchParams.get('message'); + const orderId = searchParams.get('orderId'); + + const errorMessage = getErrorMessage(code, message); + + return ( +
+
+
+ +
+

결제에 실패했습니다

+

{errorMessage}

+ + {(code || orderId) && ( +
+ {orderId && ( +
+ 주문번호 + {orderId} +
+ )} + {code && ( +
+ 에러코드 + + {code} + +
+ )} +
+ )} + +
+ + +
+
+
+ ); +} diff --git a/src/app/(service)/payment/success/page.tsx b/src/app/(service)/payment/success/page.tsx new file mode 100644 index 00000000..5491d15f --- /dev/null +++ b/src/app/(service)/payment/success/page.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useSearchParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import Button from '@/components/ui/button'; + +interface PaymentConfirmResponse { + success: boolean; + message?: string; + data?: { + orderId: string; + amount: number; + paymentKey: string; + }; +} + +// TODO: 백엔드 API 호출 함수 - 실제 API 엔드포인트로 교체 필요 +async function confirmPayment( + paymentKey: string, + orderId: string, + amount: number, +): Promise { + // TODO: 실제 백엔드 API로 교체 + // const response = await fetch('/api/payment/confirm', { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ paymentKey, orderId, amount }), + // }); + // return response.json(); + + // 임시 성공 응답 + console.log('결제 승인 요청:', { paymentKey, orderId, amount }); + + return { + success: true, + data: { + orderId, + amount, + paymentKey, + }, + }; +} + +export default function PaymentSuccessPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const [status, setStatus] = useState<'loading' | 'success' | 'error'>( + 'loading', + ); + const [errorMessage, setErrorMessage] = useState(null); + + const paymentKey = searchParams.get('paymentKey'); + const orderId = searchParams.get('orderId'); + const amount = searchParams.get('amount'); + + useEffect(() => { + async function confirm() { + if (!paymentKey || !orderId || !amount) { + setStatus('error'); + setErrorMessage('결제 정보가 올바르지 않습니다.'); + + return; + } + + try { + const result = await confirmPayment( + paymentKey, + orderId, + Number(amount), + ); + + if (result.success) { + setStatus('success'); + } else { + setStatus('error'); + setErrorMessage(result.message || '결제 승인에 실패했습니다.'); + } + } catch { + setStatus('error'); + setErrorMessage('결제 승인 중 오류가 발생했습니다.'); + } + } + + confirm(); + }, [paymentKey, orderId, amount]); + + if (status === 'loading') { + return ( +
+
+
+

+ 결제를 처리하고 있습니다... +

+
+
+ ); + } + + if (status === 'error') { + return ( +
+
+
+ +
+

결제 승인 실패

+

{errorMessage}

+ +
+
+ ); + } + + return ( +
+
+
+ +
+

결제가 완료되었습니다

+

+ 스터디 참여가 정상적으로 처리되었습니다. +

+ +
+
+ 주문번호 + {orderId} +
+
+ 결제금액 + + {Number(amount).toLocaleString()}원 + +
+
+ +
+ + +
+
+
+ ); +} diff --git a/src/app/(service)/premium-study/[id]/page.tsx b/src/app/(service)/premium-study/[id]/page.tsx new file mode 100644 index 00000000..e7c8a752 --- /dev/null +++ b/src/app/(service)/premium-study/[id]/page.tsx @@ -0,0 +1,112 @@ +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; +import type { Metadata } from 'next'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import { Configuration } from '@/api/openapi/configuration'; +import type { GroupStudyFullResponseDto } from '@/api/openapi/models'; +import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; +import { getGroupStudyMyStatusInServer } from '@/features/study/group/api/get-group-study-my-status.server'; +import { GroupStudyDetailResponse } from '@/features/study/group/api/group-study-types'; +import PremiumStudyDetailPage from '@/features/study/premium/ui/premium-study-detail-page'; +import { getServerCookie } from '@/utils/server-cookie'; + +interface Props { + params: Promise<{ id: string }>; +} + +interface GroupStudyResponse { + content?: GroupStudyFullResponseDto; +} + +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + + try { + const config = new Configuration({ + basePath: process.env.NEXT_PUBLIC_API_BASE_URL, + }); + + const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); + + const response = await groupStudyApi.getGroupStudy(Number(id)); + const groupStudy = (response.data as GroupStudyResponse)?.content; + + if (!groupStudy) { + return { + title: '멘토 스터디 - 제로원', + description: '제로원 스터디 플랫폼에서 멘토 스터디를 둘러보세요.', + }; + } + + const title = groupStudy.detailInfo?.title || '멘토 스터디'; + const description = + groupStudy.detailInfo?.description || + groupStudy.detailInfo?.summary || + '제로원 멘토 스터디에 참여하세요.'; + + return { + title: `${title} - 제로원 멘토스터디`, + description, + openGraph: { + title: `${title} - 제로원 멘토스터디`, + description, + images: groupStudy.detailInfo?.image?.resizedImages?.[0] + ?.resizedImageUrl + ? [groupStudy.detailInfo.image.resizedImages[0].resizedImageUrl] + : [], + }, + }; + } catch { + return { + title: '멘토 스터디 - 제로원', + description: '제로원 스터디 플랫폼에서 멘토 스터디를 둘러보세요.', + }; + } +} + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + const queryClient = new QueryClient(); + + // 프리미엄 스터디 상세 정보 미리 가져오기 + await queryClient.fetchQuery({ + queryKey: ['groupStudyDetail', Number(id)], + queryFn: () => getGroupStudyDetailInServer({ groupStudyId: Number(id) }), + }); + + const data: GroupStudyDetailResponse = queryClient.getQueryData([ + 'groupStudyDetail', + Number(id), + ]); + + const memberIdStr = await getServerCookie('memberId'); + const memberId = memberIdStr ? Number(memberIdStr) : undefined; + + const isLeader = data.basicInfo.leader.memberId === memberId; + + if (!isLeader && memberId) { + // 내가 리더가 아닐 경우에만 내 신청 상태 정보 미리 가져오기 + await queryClient.prefetchQuery({ + queryKey: ['groupStudyMyStatus', Number(id)], + queryFn: () => + getGroupStudyMyStatusInServer({ groupStudyId: Number(id) }), + }); + } + + return ( + + + + ); +} diff --git a/src/app/(service)/premium-study/page.tsx b/src/app/(service)/premium-study/page.tsx new file mode 100644 index 00000000..b2b8ecfc --- /dev/null +++ b/src/app/(service)/premium-study/page.tsx @@ -0,0 +1,65 @@ +import { Plus } from 'lucide-react'; +import { createApiServerInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import Button from '@/components/ui/button'; +import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; +import PremiumStudyList from '@/features/study/premium/ui/premium-study-list'; +import PremiumStudyPagination from '@/features/study/premium/ui/premium-study-pagination'; + +interface PremiumStudyPageProps { + searchParams: Promise<{ page?: string }>; +} + +export default async function PremiumStudyPage({ + searchParams, +}: PremiumStudyPageProps) { + const params = await searchParams; + const currentPage = Number(params.page) || 1; + const pageSize = 9; + + const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudies( + 'PREMIUM_STUDY', + currentPage, + pageSize, + ); + + console.log('data', data); + + return ( +
+ {/* 헤더 */} +
+

+ 멘토스터디 둘러보기 +

+ } + iconPosition="left" + > + 스터디 개설하기 + + } + /> +
+ + {/* 스터디 카드 그리드 */} + + + {/* 페이지네이션 */} + {data.totalPages > 1 && ( + + )} +
+ ); +} diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx deleted file mode 100644 index bc3bc409..00000000 --- a/src/app/(service)/study/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; -import { Configuration } from '@/api/openapi/configuration'; -import { GroupStudyFullResponseDto } from '@/api/openapi/models'; -import Button from '@/components/ui/button'; -import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; -import GroupStudyList from '@/features/study/group/ui/group-study-list'; -import IconPlus from '@/shared/icons/plus.svg'; -import { getServerCookie } from '@/utils/server-cookie'; -import Sidebar from '@/widgets/home/sidebar'; - -interface GroupStudyResponse { - content?: GroupStudyFullResponseDto; -} - -export default async function Study() { - const memberIdStr = await getServerCookie('memberId'); - const isLoggedIn = !!memberIdStr; - - // const config = new Configuration({ - // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, - // }); - - // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); - - // const response = await groupStudyApi.getGroupStudies(); - // const groupStudy = (response.data as GroupStudyResponse)?.content; - - // console.log('groupStudy', groupStudy); - - return ( -
-
-
- - 스터디 둘러보기 - - } - > - 스터디 개설하기 - - } - /> -
- -
- {isLoggedIn && } -
- ); -} diff --git a/src/components/payment/PaymentTermsModal.tsx b/src/components/payment/PaymentTermsModal.tsx new file mode 100644 index 00000000..dedcf478 --- /dev/null +++ b/src/components/payment/PaymentTermsModal.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { XIcon } from 'lucide-react'; +import Button from '../ui/button'; +import { Modal } from '../ui/modal'; + +export default function PaymentTermsModal() { + return ( + + + 내용보기 + + + + + + ZeroOne IT 서비스 이용약관 + + + + + +

서비스 이용약관

+ +
+

1. 결제 및 서비스 제공

+

+ 본 스터디 프로그램은 4주 과정의 온라인 학습 서비스이며, 결제 + 완료 시 즉시 서비스 이용이 가능하도록 구성됩니다. +

+
+ +
+

2. 환불 가능 기간

+

+ 서비스 특성상 다음과 같은 환불 기준을 적용합니다. +

+ +
+
+

• 전액 환불

+
    +
  • 결제일로부터 7일 이내
  • +
  • + 스터디 자료 열람 및 참여 이력이 0회일 경우 가능(예: 강의 + 시청, 자료 다운로드, 미션 수행, 커뮤니티 글 열람/참여 등) +
  • +
+
+ +
+

• 부분 환불

+
    +
  • + 아래 조건을 모두 충족할 경우 부분 환불이 가능합니다. +
  • +
  • 결제일로부터 7일 이내
  • +
  • 스터디 자료 사용 또는 참여 이력이 있을 경우
  • +
  • + 계산 방식 : 환불액 = 결제금액 - 총 금액 × (이용일수/총 + 스터디 일수) +
  • +
  • 콘텐츠/세션 이용 횟수에 따른 차감 후 환불
  • +
+
+ +
+

• 환불 불가

+
    +
  • 결제일로부터 7일 경과 후
  • +
  • 서비스 기간의 50% 이상 이용 시
  • +
  • + 스터디 자료 다운로드, 핵심 콘텐츠 접근, 참여 등으로 인해 + 회수 불가능한 가치 제공이 완료된 경우 +
  • +
  • + 회원의 과실, 단순 변심, 개인 일정 등의 사유로 서비스 이용이 + 어려운 경우 +
  • +
+
+
+
+ +
+

3. 환불 접수 방법

+
    +
  1. 환불 요청은 이메일 또는 채널톡/문의 폼을 통해 접수
  2. +
  3. + 환불 시 신분 확인을 위해 결제 정보 및 환불 계좌가 필요할 수 + 있음 +
  4. +
  5. 환불 처리는 접수 후 영업일 기준 3~5일 소요될 수 있음
  6. +
+
+ +
+

4. 강제 퇴장/정책 위반

+

+ 커뮤니티 규칙 위반, 불법 공유, 타인 비방 등 운영 정책을 위반한 + 경우 별도 환불 없이 서비스 이용이 제한될 수 있습니다. +

+
+
+ + + + + +
+
+
+ ); +} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx index 77a77253..c6e86371 100644 --- a/src/components/payment/paymentActionClient.tsx +++ b/src/components/payment/paymentActionClient.tsx @@ -1,15 +1,22 @@ 'use client'; import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; -import { useEffect, useMemo, useState } from 'react'; -import { cn } from '../ui/(shadcn)/lib/utils'; +import { useEffect, useState } from 'react'; import Button from '../ui/button'; import Checkbox from '../ui/checkbox'; import { RadioGroup, RadioGroupItem } from '../ui/radio'; +import PaymentTermsModal from './PaymentTermsModal'; + +interface Study { + id: string; + title: string; + desc: string; + price: number; + thumbnailUrl?: string; +} interface Props { - orderId: string; - amount: number; + study: Study; } type PaymentMethod = 'CARD' | 'VBANK'; @@ -21,10 +28,7 @@ const methods: { id: PaymentMethod; label: string }[] = [ { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, ]; -export default function PaymentCheckoutPage({ - orderId, - // amount, -}: Props) { +export default function PaymentCheckoutPage({ study }: Props) { const [payment, setPayment] = useState(null); const [isAgreed, setIsAgreed] = useState(false); @@ -55,19 +59,21 @@ export default function PaymentCheckoutPage({ try { await payment.requestPayment({ - method: 'CARD', // 카드 및 간편결제 - amount: 50000, // 결제 금액 - orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 - orderName: '토스 티셔츠 외 2건', - successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL - failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + method: paymentMethod, + amount: { + currency: 'KRW', + value: study.price, + }, + orderId: study.id, + orderName: study.title, + successUrl: window.location.origin + '/payment/success', + failUrl: window.location.origin + '/payment/fail', customerEmail: 'customer123@gmail.com', customerName: '김토스', customerMobilePhone: '01012341234', - // 카드 결제에 필요한 정보 card: { useEscrow: false, - flowMode: 'DEFAULT', // 통합결제창 여는 옵션 + flowMode: 'DEFAULT', useCardPoint: false, useAppCardOnly: false, }, @@ -83,10 +89,6 @@ export default function PaymentCheckoutPage({ } }; - const [amount] = useState({ - currency: 'KRW', - value: 50000, - }); useEffect(() => { async function fetchPayment() { @@ -127,14 +129,7 @@ export default function PaymentCheckoutPage({
- - 내용보기 - +
@@ -151,18 +146,18 @@ export default function PaymentCheckoutPage({ const selected = paymentMethod === m.id; return ( - + + {m.label} + ); })} diff --git a/src/features/my-page/ui/my-study-info-card.tsx b/src/features/my-page/ui/my-study-info-card.tsx index 5f5ee735..42eb540d 100644 --- a/src/features/my-page/ui/my-study-info-card.tsx +++ b/src/features/my-page/ui/my-study-info-card.tsx @@ -27,7 +27,7 @@ export default function MyStudyInfoCard({ return (
  • - +
    => { + const { page, size, status } = params; + + const { data } = await axiosServerInstance.get('/group-studies', { + params: { + page, + 'page-size': size, + groupStudyStatus: status, + }, + }); + + if (data.statusCode !== 200) { + throw new Error('Failed to fetch group study list'); + } + + return data.content; +}; diff --git a/src/features/study/group/api/get-group-study-list.ts b/src/features/study/group/api/get-group-study-list.ts index 4efe79c2..9fef5779 100644 --- a/src/features/study/group/api/get-group-study-list.ts +++ b/src/features/study/group/api/get-group-study-list.ts @@ -8,7 +8,7 @@ import { export const getGroupStudyList = async ( params: GroupStudyListRequest, ): Promise => { - const { page, size, status } = params; + const { page, size, status, classification } = params; try { const { data } = await axiosInstance.get('/group-studies', { @@ -16,6 +16,7 @@ export const getGroupStudyList = async ( page, 'page-size': size, groupStudyStatus: status, + classification: classification, }, }); diff --git a/src/features/study/group/model/group-study-form.schema.ts b/src/features/study/group/model/group-study-form.schema.ts index c05b650f..e3812a95 100644 --- a/src/features/study/group/model/group-study-form.schema.ts +++ b/src/features/study/group/model/group-study-form.schema.ts @@ -11,7 +11,11 @@ import { const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; +export const STUDY_CLASSIFICATION = ['GROUP_STUDY', 'PREMIUM_STUDY'] as const; +export type StudyClassification = (typeof STUDY_CLASSIFICATION)[number]; + export const GroupStudyFormSchema = z.object({ + classification: z.enum(STUDY_CLASSIFICATION), type: z.enum(STUDY_TYPES), targetRoles: z .array(z.enum(TARGET_ROLE_OPTIONS)) @@ -60,8 +64,11 @@ export type GroupStudyFormValues = z.input & { }; export type OpenGroupParsedValues = z.output; -export function buildOpenGroupDefaultValues(): GroupStudyFormValues { +export function buildOpenGroupDefaultValues( + classification: StudyClassification = 'GROUP_STUDY', +): GroupStudyFormValues { return { + classification, type: 'PROJECT', targetRoles: [], maxMembersCount: '', @@ -80,9 +87,9 @@ export function buildOpenGroupDefaultValues(): GroupStudyFormValues { }; } -export function toOpenGroupRequest( - v: OpenGroupParsedValues, -): GroupStudyFormRequest { +export function toOpenGroupRequest(v: OpenGroupParsedValues): GroupStudyFormRequest { + const isPremiumStudy = v.classification === 'PREMIUM_STUDY'; + return { basicInfo: { type: v.type, @@ -94,7 +101,7 @@ export function toOpenGroupRequest( location: v.location.trim(), startDate: v.startDate.trim(), endDate: v.endDate.trim(), - price: Number(v.price), + price: isPremiumStudy ? Number(v.price) || 0 : 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, diff --git a/src/features/study/group/model/use-group-study-list-query.ts b/src/features/study/group/model/use-group-study-list-query.ts index 4b6b3c60..8a236c45 100644 --- a/src/features/study/group/model/use-group-study-list-query.ts +++ b/src/features/study/group/model/use-group-study-list-query.ts @@ -9,6 +9,7 @@ export const useGroupStudyListQuery = () => { page: pageParam, size: 20, status: 'RECRUITING', + classification: 'GROUP_STUDY', }); return response; diff --git a/src/features/study/group/ui/group-study-form-modal.tsx b/src/features/study/group/ui/group-study-form-modal.tsx index 4f6ef0ef..47748deb 100644 --- a/src/features/study/group/ui/group-study-form-modal.tsx +++ b/src/features/study/group/ui/group-study-form-modal.tsx @@ -19,16 +19,20 @@ import { import { buildOpenGroupDefaultValues, GroupStudyFormValues, + StudyClassification, toOpenGroupRequest, } from '../model/group-study-form.schema'; import { useGroupStudyDetailQuery } from '../model/use-study-query'; +export type { StudyClassification }; + interface GroupStudyModalProps { trigger?: React.ReactNode; open?: boolean; onOpenChange?: () => void; mode: 'create' | 'edit'; groupStudyId?: number; + classification?: StudyClassification; } export default function GroupStudyFormModal({ @@ -37,6 +41,7 @@ export default function GroupStudyFormModal({ open: controlledOpen = false, groupStudyId, onOpenChange: onControlledOpen, + classification = 'GROUP_STUDY', }: GroupStudyModalProps) { const qc = useQueryClient(); const [open, setOpen] = useState(false); @@ -81,6 +86,7 @@ export default function GroupStudyFormModal({ if (isLoading) return; return { + classification, type: value.basicInfo.type, targetRoles: value.basicInfo.targetRoles, maxMembersCount: value.basicInfo.maxMembersCount.toString(), @@ -213,7 +219,7 @@ export default function GroupStudyFormModal({ ('GROUP_STUDY'); +export const useClassification = () => useContext(ClassificationContext); + interface GroupStudyFormProps { defaultValues: GroupStudyFormValues; onSubmit: (values: GroupStudyFormValues) => void; @@ -27,6 +31,7 @@ const STEP_FIELDS: Record<1 | 2 | 3, (keyof GroupStudyFormValues)[]> = { 'regularMeeting', 'startDate', 'endDate', + 'price', ], 2: ['thumbnailExtension', 'title', 'description', 'summary'], 3: ['interviewPost'], @@ -42,7 +47,8 @@ export default function GroupStudyForm({ defaultValues: defaultValues, }); - const { handleSubmit, trigger, formState } = methods; + const { handleSubmit, trigger, formState, watch } = methods; + const classification = watch('classification'); const [step, setStep] = useState<1 | 2 | 3>(1); @@ -63,7 +69,7 @@ export default function GroupStudyForm({ }; return ( - <> + @@ -124,7 +130,7 @@ export default function GroupStudyForm({ )}
    - + ); } diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 6f355e81..431f3729 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -1,199 +1,159 @@ 'use client'; import { sendGTMEvent } from '@next/third-parties/google'; -import { Loader2 } from 'lucide-react'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; +import Link from 'next/link'; import Badge from '@/components/ui/badge'; -import { useIntersectionObserver } from '@/hooks/common/use-intersection-observer'; +import Button from '@/components/ui/button'; import { useAuth } from '@/hooks/use-auth'; import { hashValue } from '@/utils/hash'; -import { BasicInfoDetail } from '../api/group-study-types'; -import { - EXPERIENCE_LEVEL_LABELS, - REGULAR_MEETING_LABELS, - ROLE_LABELS, - STUDY_TYPE_LABELS, -} from '../const/group-study-const'; -import { useGroupStudyListQuery } from '../model/use-group-study-list-query'; +import { GroupStudyData } from '../api/group-study-types'; +import { REGULAR_MEETING_LABELS } from '../const/group-study-const'; interface GroupStudyListProps { - isLoggedIn: boolean; + studies: GroupStudyData[]; } -export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { - const router = useRouter(); +export default function GroupStudyList({ studies }: GroupStudyListProps) { const { data: authData } = useAuth(); - const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = - useGroupStudyListQuery(); - const groupStudyList = data?.pages.flatMap((page) => page.content) || []; - - // useIntersectionObserver 커스텀 훅 사용 - const sentinelRef = useIntersectionObserver( - async () => { - if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage(); - } - }, - { - rootMargin: '200px 0px', - enabled: !!hasNextPage, - }, - ); - - const basicInfoItems = (basicInfo: BasicInfoDetail) => { - const { - type, - targetRoles, - experienceLevels, - regularMeeting, - maxMembersCount, - price, - approvedCount, - } = basicInfo; - - // 타입 변환 - const typeLabel = STUDY_TYPE_LABELS[type]; - - // 역할 변환 - const targetRolesLabel = targetRoles - .map((role) => { - return ROLE_LABELS[role]; - }) - .join(', '); - - // 경력 변환 - const experienceLabel = - experienceLevels - .map((level) => { - return EXPERIENCE_LEVEL_LABELS[level]; - }) - .join(', ') || '무관'; - - // 정기모임 - const frequencyLabel = REGULAR_MEETING_LABELS[regularMeeting]; - - // 참가비 - const priceLabel = price === 0 ? '무료' : `${price.toLocaleString()}원`; - - return [ - { label: '유형', value: typeLabel }, - { label: '주제', value: targetRolesLabel }, - { label: '경력', value: experienceLabel }, - { label: '정기모임', value: frequencyLabel }, - { label: '모집인원', value: `${approvedCount}/${maxMembersCount}` }, - { label: '참가비', value: priceLabel }, - ]; + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'group_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); }; - if (isLoading) { + if (studies.length === 0) { return ( -
    - +
    + 현재 그룹 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    ); } return ( - <> - {groupStudyList.length > 0 ? ( - <> -
    - {groupStudyList.map((study, index) => { - return ( -
    { - sendGTMEvent({ - event: 'group_study_detail_view', - dl_timestamp: new Date().toISOString(), - ...(authData?.memberId && { - dl_member_id: hashValue(String(authData.memberId)), - }), - dl_study_id: String(study.basicInfo.groupStudyId), - dl_study_title: study.simpleDetailInfo.title, - }); - router.push(`study/${study.basicInfo.groupStudyId}`); - }} - > -
    -
    -
    - {study.basicInfo.hostType === 'ZEROONE' && ( - 제로원 스터디 - )} - - - {study.simpleDetailInfo.title} - -
    -

    - {study.simpleDetailInfo.summary} -

    -
    -
    - {basicInfoItems(study.basicInfo).map((item, idx) => ( -
    - - {item.label} - - - {item.value} - -
    - ))} -
    -
    - {/* thumbnail */} -
    - ); - })} -
    - {/* 아래는 다음 페이지 데이터를 불러오기 위한 요소입니다. */} -
    } - className="h-10 w-full" - > - {isFetchingNextPage && ( -
    - +
    + {studies.map((study) => ( + handleStudyClick(study)} + className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" + > + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} + {study.basicInfo.hostType === 'ZEROONE' && ( +
    + 제로원 스터디
    )}
    - - ) : ( -
    - 현재 그룹 스터디가 없습니다. -
    - - 스터디가 아직 준비되지 않았습니다. - - - 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. - + {/* 컨텐츠 영역 */} +
    + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 리더 정보 */} +
    +
    +
    + {/* {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} */} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    +
    + + {study.basicInfo.approvedCount}/ + {study.basicInfo.maxMembersCount}명 + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 가격 및 버튼 */} +
    + + {study.basicInfo.price === 0 + ? '무료' + : `${study.basicInfo.price.toLocaleString()}원`} + + +
    -
    - )} - + + ))} +
    ); } diff --git a/src/features/study/group/ui/group-study-pagination.tsx b/src/features/study/group/ui/group-study-pagination.tsx new file mode 100644 index 00000000..9219a6c6 --- /dev/null +++ b/src/features/study/group/ui/group-study-pagination.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import Pagination from '@/components/ui/pagination'; + +interface GroupStudyPaginationProps { + currentPage: number; + totalPages: number; +} + +export default function GroupStudyPagination({ + currentPage, + totalPages, +}: GroupStudyPaginationProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleChangePage = (page: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', String(page)); + router.push(`/group-study?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index 8c7508aa..600126ba 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -23,6 +23,7 @@ import { REGULAR_MEETING_LABELS, } from '../../const/group-study-const'; import { GroupStudyFormValues } from '../../model/group-study-form.schema'; +import { useClassification } from '../group-study-form'; const methodOptions = STUDY_METHODS.map((v) => ({ label: STUDY_METHOD_LABELS[v], @@ -37,6 +38,9 @@ const memberOptions = Array.from({ length: 20 }, (_, i) => { export default function Step1OpenGroupStudy() { const { control, formState, watch } = useFormContext(); + const classification = useClassification(); + const isPremiumStudy = classification === 'PREMIUM_STUDY'; + const { field: typeField } = useController({ name: 'type', control, @@ -233,17 +237,29 @@ export default function Step1OpenGroupStudy() { )}
    - {/* API에는 있는데 디자인에는 없음. 뭐지??? */} - {/* - name="price" - label="참가비" - helper="참가비가 있다면 입력해주세요. (0원 가능)" - direction="vertical" - size="medium" - required - > - - */} + {isPremiumStudy && ( + + name="price" + label="참가비" + helper="참가비가 있다면 입력해주세요. (0원 가능)" + direction="vertical" + size="medium" + > + ( + + )} + /> + + )} ); } diff --git a/src/features/study/premium/ui/premium-study-detail-page.tsx b/src/features/study/premium/ui/premium-study-detail-page.tsx new file mode 100644 index 00000000..81a7c42e --- /dev/null +++ b/src/features/study/premium/ui/premium-study-detail-page.tsx @@ -0,0 +1,238 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import MoreMenu from '@/components/ui/dropdown/more-menu'; +import Tabs from '@/components/ui/tabs'; +import { useLeaderStore } from '@/stores/useLeaderStore'; +import ConfirmDeleteModal from '../../group/ui/confirm-delete-modal'; +import GroupStudyFormModal from '../../group/ui/group-study-form-modal'; +import GroupStudyMemberList from '../../group/ui/group-study-member-list'; +import PremiumStudyInfoSection from './premium-study-info-section'; +import ChannelSection from '../../group/channel/ui/channel-section'; +import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; +import { + useCompleteGroupStudyMutation, + useDeleteGroupStudyMutation, + useGroupStudyDetailQuery, +} from '../../group/model/use-study-query'; + +type ActiveTab = 'intro' | 'members' | 'channel'; + +type ActionKey = 'end' | 'delete'; + +interface PremiumStudyDetailPageProps { + groupStudyId: number; + memberId?: number; +} + +const TABS = [ + { label: '스터디 소개', value: 'intro' }, + { label: '참가자', value: 'members' }, + { label: '채널', value: 'channel' }, +]; + +export default function PremiumStudyDetailPage({ + groupStudyId, + memberId, +}: PremiumStudyDetailPageProps) { + const router = useRouter(); + + const { data: studyDetail, isLoading } = + useGroupStudyDetailQuery(groupStudyId); + + const leaderId = studyDetail?.basicInfo.leader.memberId; + + const isLeader = leaderId === memberId; + + const [active, setActive] = useState('intro'); + const [showModal, setShowModal] = useState(false); + const [action, setAction] = useState(null); + const [showStudyFormModal, setShowStudyFormModal] = useState(false); + + const setLeaderInfo = useLeaderStore((s) => s.setLeaderInfo); + + const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ + groupStudyId, + isLeader, + }); + + useEffect(() => { + const leader = studyDetail.basicInfo.leader; + setLeaderInfo(leader); + }, [studyDetail, setLeaderInfo]); + + const { mutate: deleteGroupStudy } = useDeleteGroupStudyMutation(); + const { mutate: completeStudy } = useCompleteGroupStudyMutation(); + + const ModalContent = { + end: { + title: '스터디를 종료하시겠어요?', + content: ( + <> + 종료 후에는 더 이상 모집/활동이 불가합니다. +
    이 동작은 되돌릴 수 없습니다. + + ), + confirmText: '스터디 종료', + onConfirm: () => { + completeStudy( + { groupStudyId }, + { + onSuccess: () => { + sendGTMEvent({ + event: 'premium_study_end', + group_study_id: String(groupStudyId), + }); + alert('스터디가 종료되었습니다.'); + }, + onSettled: () => { + setShowModal(false); + router.push('/premium-study'); + }, + }, + ); + }, + }, + delete: { + title: '스터디를 삭제하시겠어요?', + content: ( + <> + 삭제 시 모든 데이터가 영구적으로 제거됩니다. +
    이 동작은 되돌릴 수 없습니다. + + ), + confirmText: '스터디 삭제', + onConfirm: () => { + deleteGroupStudy( + { groupStudyId }, + { + onSuccess: () => { + sendGTMEvent({ + event: 'premium_study_delete', + group_study_id: String(groupStudyId), + }); + alert('스터디가 삭제되었습니다.'); + }, + onError: () => { + alert('스터디 삭제에 실패하였습니다.'); + }, + onSettled: () => { + router.push('/premium-study'); + setShowModal(false); + }, + }, + ); + }, + }, + }; + + // 참가자, 채널 탭 접근 가능 여부 = 스터디 참가자 또는 방장만 가능 + const isMember = + myApplicationStatus?.status === 'APPROVED' || + myApplicationStatus?.status === 'KICKED'; + + if (isLoading || !studyDetail) { + return
    로딩중...
    ; + } + + return ( +
    + setShowModal(!showModal)} + title={ModalContent[action]?.title} + content={ModalContent[action]?.content} + confirmText={ModalContent[action]?.confirmText} + onConfirm={ModalContent[action]?.onConfirm} + /> + setShowStudyFormModal(!showStudyFormModal)} + /> + +
    +
    +

    + {studyDetail?.detailInfo.title} +

    +

    + {studyDetail?.detailInfo.summary} +

    +
    + {memberId === studyDetail.basicInfo.leader.memberId && ( + { + setShowStudyFormModal(true); + }, + }, + { + label: '스터디 종료', + value: 'end', + onMenuClick: () => { + setAction('end'); + setShowModal(true); + }, + }, + { + label: '스터디 삭제', + value: 'delete', + onMenuClick: () => { + setAction('delete'); + setShowModal(true); + }, + }, + ]} + iconSize={35} + /> + )} +
    + + {/** 탭리스트 */} + tab.value === 'intro' || isLeader || isMember, + )} + activeTab={active} + onChange={(value: ActiveTab) => { + setActive(value); + sendGTMEvent({ + event: 'premium_study_tab_change', + group_study_id: String(groupStudyId), + tab: value, + }); + }} + /> + {active === 'intro' && ( + + )} + {active === 'members' && ( + + )} + {active === 'channel' && ( + + )} +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-info-section.tsx b/src/features/study/premium/ui/premium-study-info-section.tsx new file mode 100644 index 00000000..6066407a --- /dev/null +++ b/src/features/study/premium/ui/premium-study-info-section.tsx @@ -0,0 +1,326 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import dayjs from 'dayjs'; +import { + Calendar, + Clock, + File, + Folder, + Globe, + HandCoins, + MapPin, + SignpostBig, + UserCheck, + Users, +} from 'lucide-react'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import React from 'react'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import UserAvatar from '@/components/ui/avatar'; +import Button from '@/components/ui/button'; +import { getSincerityPresetByLevelName } from '@/config/sincerity-temp-presets'; +import UserProfileModal from '@/entities/user/ui/user-profile-modal'; +import { useAuth } from '@/hooks/use-auth'; +import { hashValue } from '@/utils/hash'; +import InfoCard from '@/widgets/study/group/ui/group-detail/info-card'; +import PremiumSummaryStudyInfo from './premium-summary-study-info'; + +import { + BasicInfoDetail, + GroupStudyDetailResponse, +} from '../../group/api/group-study-types'; + +import { useApplicantsByStatusQuery } from '../../group/application/model/use-applicant-qeury'; +import { + EXPERIENCE_LEVEL_LABELS, + REGULAR_MEETING_LABELS, + ROLE_LABELS, + STUDY_METHOD_LABELS, + STUDY_STATUS_LABELS, + STUDY_TYPE_LABELS, +} from '../../group/const/group-study-const'; + +interface PremiumStudyInfoSectionProps { + study: GroupStudyDetailResponse; + groupStudyId: number; + isLeader: boolean; + memberId?: number; +} + +export default function PremiumStudyInfoSection({ + study: studyDetail, + groupStudyId, + isLeader, + memberId, +}: PremiumStudyInfoSectionProps) { + const router = useRouter(); + const { data: authData } = useAuth(); + + const { data: approvedApplicants } = useApplicantsByStatusQuery({ + groupStudyId, + status: 'APPROVED', + }); + const { data: pendingApplicants } = useApplicantsByStatusQuery({ + groupStudyId, + status: 'PENDING', + }); + + const applicants = [ + ...(approvedApplicants?.pages.flatMap(({ content }) => content) || []), + ...(pendingApplicants?.pages.flatMap(({ content }) => content) || []), + ]; + + const basicInfoItems = (basicInfo: BasicInfoDetail) => { + const getDurationText = (startDate: string, endDate: string): string => { + const start = new Date(startDate); + const end = new Date(endDate); + + const diffTime = end.getTime() - start.getTime(); + if (diffTime < 0) return '기간이 잘못되었습니다.'; + + const diffDays = diffTime / (1000 * 60 * 60 * 24); + const diffWeeks = diffDays / 7; + const diffMonths = diffDays / 30; + + return diffMonths < 1 + ? `약 ${Math.round(diffWeeks)}주` + : `약 ${Math.round(diffMonths)}개월`; + }; + + return [ + { + label: '유형', + value: STUDY_TYPE_LABELS[basicInfo.type], + icon: , + }, + { + label: '주제', + value: basicInfo.targetRoles + .map((role) => { + return ROLE_LABELS[role]; + }) + .join(', '), + icon: , + }, + { + label: '경력', + value: + basicInfo.experienceLevels + .map((level) => { + return EXPERIENCE_LEVEL_LABELS[level]; + }) + .join(', ') || '무관', + icon: , + }, + { + label: '진행 방식', + value: `${STUDY_METHOD_LABELS[basicInfo.method]}${basicInfo.location ? `, ${basicInfo.location}` : ''}`, + icon: , + }, + { + label: '진행 기간', + value: getDurationText(basicInfo.startDate, basicInfo.endDate), + icon: , + }, + { + label: '정기모임', + value: REGULAR_MEETING_LABELS[basicInfo.regularMeeting], + icon: , + }, + { + label: '모집인원', + value: `${basicInfo.maxMembersCount}명`, + icon: , + }, + { + label: '시작일자', + value: dayjs(basicInfo.startDate).format('YYYY.MM.DD'), + icon: , + }, + { + label: '참가비', + value: + basicInfo.price === 0 + ? '무료' + : `${basicInfo.price.toLocaleString()}원`, + icon: , + }, + { + label: '상태', + value: `${STUDY_STATUS_LABELS[basicInfo.status]}`, + icon: , + }, + ]; + }; + + const summaryBasicInfoItems = (basicInfo: BasicInfoDetail) => { + return [ + { + label: '주제', + value: basicInfo.targetRoles + .map((role) => { + return ROLE_LABELS[role]; + }) + .join(', '), + icon: , + }, + { + label: '정기모임', + value: `${REGULAR_MEETING_LABELS[basicInfo.regularMeeting]}, ${basicInfo.location}`, + icon: , + }, + { + label: '경력', + value: + basicInfo.experienceLevels + .map((level) => { + return EXPERIENCE_LEVEL_LABELS[level]; + }) + .join(', ') || '무관', + icon: , + }, + { + label: '모집인원', + value: `${basicInfo.maxMembersCount}명`, + icon: , + }, + ]; + }; + + return ( +
    +
    +
    + 썸네일 +
    + +
    +
    +

    스터디 소개

    +
    + {studyDetail?.detailInfo.description} +
    +
    +
    +

    기본 정보

    +
    + {basicInfoItems(studyDetail?.basicInfo).map((item) => { + return ( + + ); + })} +
    +
    +
    +
    +
    + 실시간 신청자 목록 + {`${studyDetail.basicInfo.approvedCount + studyDetail.basicInfo.pendingCount}명`} +
    + {isLeader && ( + + )} +
    + +
    + {applicants.map((data) => { + const temperPreset = getSincerityPresetByLevelName( + data.applicantInfo.sincerityTemp.levelName as string, + ); + + return ( +
    + +
    +
    +
    + {data.applicantInfo.memberNickname !== '' + ? data.applicantInfo.memberNickname + : '익명'} +
    + + {`${data.applicantInfo.sincerityTemp.temperature}`}℃ + +
    +
    + { + sendGTMEvent({ + event: 'premium_study_member_profile_click', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue( + String(authData.memberId), + ), + }), + dl_target_member_id: String( + data.applicantInfo.memberId, + ), + dl_group_study_id: String(groupStudyId), + }); + }} + > + 프로필 +
    + } + /> +
    + ); + })} +
    +
    +
    +
    + +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-list.tsx b/src/features/study/premium/ui/premium-study-list.tsx new file mode 100644 index 00000000..5c18244a --- /dev/null +++ b/src/features/study/premium/ui/premium-study-list.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import Image from 'next/image'; +import Link from 'next/link'; +import Badge from '@/components/ui/badge'; +import Button from '@/components/ui/button'; +import { useAuth } from '@/hooks/use-auth'; +import { hashValue } from '@/utils/hash'; + +import { GroupStudyData } from '../../group/api/group-study-types'; +import { REGULAR_MEETING_LABELS } from '../../group/const/group-study-const'; + +interface PremiumStudyListProps { + studies: GroupStudyData[]; +} + +export default function PremiumStudyList({ studies }: PremiumStudyListProps) { + const { data: authData } = useAuth(); + + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'premium_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); + }; + + if (studies.length === 0) { + return ( +
    + 현재 멘토 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    +
    + ); + } + + return ( +
    + {studies.map((study) => ( + handleStudyClick(study)} + className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" + > + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} + {study.basicInfo.hostType === 'ZEROONE' && ( +
    + 제로원 스터디 +
    + )} +
    + + {/* 컨텐츠 영역 */} +
    + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 리더 정보 */} +
    +
    +
    + {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    +
    + + {study.basicInfo.approvedCount}/ + {study.basicInfo.maxMembersCount}명 + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 가격 및 버튼 */} +
    + + {study.basicInfo.price === 0 + ? '무료' + : `${study.basicInfo.price.toLocaleString()}원`} + + +
    +
    + + ))} +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-pagination.tsx b/src/features/study/premium/ui/premium-study-pagination.tsx new file mode 100644 index 00000000..51851adb --- /dev/null +++ b/src/features/study/premium/ui/premium-study-pagination.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import Pagination from '@/components/ui/pagination'; + +interface PremiumStudyPaginationProps { + currentPage: number; + totalPages: number; +} + +export default function PremiumStudyPagination({ + currentPage, + totalPages, +}: PremiumStudyPaginationProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleChangePage = (page: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', String(page)); + router.push(`/premium-study?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/src/features/study/premium/ui/premium-summary-study-info.tsx b/src/features/study/premium/ui/premium-summary-study-info.tsx new file mode 100644 index 00000000..3fa717bc --- /dev/null +++ b/src/features/study/premium/ui/premium-summary-study-info.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import React from 'react'; +import Button from '@/components/ui/button'; +import { + GroupStudyDetailResponse, + GroupStudyStatus, +} from '../../group/api/group-study-types'; +import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; + +interface PremiumSummaryStudyInfoProps { + data: { + label: string; + value: string; + icon: React.ReactNode; + }[]; + title: string; + groupStudyId: number; + questions: GroupStudyDetailResponse['interviewPost']['interviewPost']; + isLeader: boolean; + groupStudyStatus: GroupStudyStatus; + approvedCount: GroupStudyDetailResponse['basicInfo']['approvedCount']; + maxMembersCount: GroupStudyDetailResponse['basicInfo']['maxMembersCount']; + price: number; + memberId?: number; +} + +export default function PremiumSummaryStudyInfo({ + data, + title, + groupStudyId, + isLeader, + groupStudyStatus, + approvedCount, + maxMembersCount, + price, + memberId, +}: PremiumSummaryStudyInfoProps) { + const router = useRouter(); + const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ + groupStudyId, + isLeader, + }); + + const isLoggedIn = typeof memberId === 'number'; + + const handleCopyURL = async () => { + await navigator.clipboard.writeText(window.location.href); + alert('스터디 링크가 복사되었습니다!'); + }; + + const handleApplyClick = () => { + // 결제 페이지로 이동 (groupStudyId를 전달) + router.push(`/payment/${groupStudyId}`); + }; + + const isApplyDisabled = + myApplicationStatus?.status !== 'NONE' || + groupStudyStatus === 'IN_PROGRESS' || + approvedCount >= maxMembersCount; + + const getButtonText = () => { + if ( + myApplicationStatus?.status === 'APPROVED' || + groupStudyStatus === 'IN_PROGRESS' + ) { + return '참여 중인 스터디'; + } + if (myApplicationStatus?.status === 'PENDING') { + return '승인 대기중'; + } + return '신청하기'; + }; + + return ( +
    +

    {title}

    +
    +
    + {data.map((item) => ( +
    +
    {item.icon}
    + + {item.value} + +
    + ))} +
    + + {/* 가격 표시 */} +
    + 참가비 + + {price === 0 ? '무료' : `${price.toLocaleString()}원`} + +
    + +
    + {!isLeader && isLoggedIn && ( + + )} + {!isLoggedIn && ( + + )} + + +
    +
    + ); +} diff --git a/src/widgets/home/header.tsx b/src/widgets/home/header.tsx index 77d5c42b..3bdfa740 100644 --- a/src/widgets/home/header.tsx +++ b/src/widgets/home/header.tsx @@ -45,8 +45,8 @@ export default async function Header() { {/* 1차 MVP에선 사용하지 않아 제외 */} From 3afc086708a13ed5eb6ce12856e4b3d1b2518ee9 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 20 Dec 2025 23:17:18 +0900 Subject: [PATCH 003/211] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/app/(service)/payment/[id]/page.tsx | 102 ++++++++++ src/app/(service)/payment/complete/page.tsx | 125 ++++++++++++ src/app/(service)/study/page.tsx | 18 ++ src/components/payment/orderSummary.tsx | 27 +++ .../payment/paymentActionClient.tsx | 183 ++++++++++++++++++ src/components/payment/priceSummary.tsx | 24 +++ src/components/ui/radio/index.tsx | 4 +- .../study/group/ui/group-study-list.tsx | 4 +- yarn.lock | 5 + 10 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 src/app/(service)/payment/[id]/page.tsx create mode 100644 src/app/(service)/payment/complete/page.tsx create mode 100644 src/components/payment/orderSummary.tsx create mode 100644 src/components/payment/paymentActionClient.tsx create mode 100644 src/components/payment/priceSummary.tsx diff --git a/package.json b/package.json index 3e61c778..3aa4020e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.0.6", + "@tosspayments/tosspayments-sdk": "^2.5.0", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/app/(service)/payment/[id]/page.tsx b/src/app/(service)/payment/[id]/page.tsx new file mode 100644 index 00000000..a51a8d69 --- /dev/null +++ b/src/app/(service)/payment/[id]/page.tsx @@ -0,0 +1,102 @@ +// app/checkout/page.tsx + +import OrderSummary from '@/components/payment/orderSummary'; +import PaymentCheckoutPage from '@/components/payment/paymentActionClient'; +import CheckoutActionClient from '@/components/payment/paymentActionClient'; +import PriceSummary from '@/components/payment/priceSummary'; + +interface Study { + id: string; + title: string; + desc: string; + price: number; + thumbnailUrl?: string; +} + +interface Terms { + id: string; + label: string; + required: boolean; + url?: string; +} + +interface PaymentMethod { + id: 'CARD' | 'VBANK'; + label: string; + subLabel?: string; +} + +async function getCheckoutData(): Promise<{ + study: Study; + terms: Terms[]; + methods: PaymentMethod[]; +}> { + // 실제론 db / internal api에서 가져오기 + return { + study: { + id: 'study_1', + title: '1일1코테류프를 인증 챌린지', + desc: '1인 개발자, 이제는 대세가 되었다죠.', + price: 35000, + thumbnailUrl: '', + }, + terms: [ + { + id: 'terms_usage', + label: '이용약관 동의 (필수)', + required: true, + url: '/terms/usage', + }, + ], + methods: [ + { id: 'CARD', label: '신용카드 결제' }, + { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, + ], + }; +} + +export default async function CheckoutPage() { + const { study, terms, methods } = await getCheckoutData(); + + // const clientKey = 'test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm'; + // const tossPayments = await loadTossPayments(clientKey); + + // const customerKey = "pfROxUh11lHWaPrBxzIBN"; + // const widgets = tossPayments.widgets({ + // customerKey, + // }); + + return ( +
    +
    +
    + {/* 서버 렌더: 선택한 스터디 */} +
    +

    선택한 스터디

    + + +
    + + {/* 서버 렌더: 결제 금액 */} +
    +

    결제 금액

    + +
    + +
    +
    + + {/* 클라 렌더: 약관/결제수단/결제하기 */} +
    + +
    +
    +
    +
    + ); +} diff --git a/src/app/(service)/payment/complete/page.tsx b/src/app/(service)/payment/complete/page.tsx new file mode 100644 index 00000000..2d07c9ed --- /dev/null +++ b/src/app/(service)/payment/complete/page.tsx @@ -0,0 +1,125 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import Button from '@/components/ui/button'; + +interface PaymentResult { + orderName: string; + productAmount: number; + paymentMethod: string; + totalAmount: number; +} + +// ✅ 가데이터 +const MOCK_PAYMENT_RESULT: PaymentResult = { + orderName: '1일코테문풀 인증 챌린지', + productAmount: 35000, + paymentMethod: '카드결제', + totalAmount: 35000, +}; + +export default function PaymentCompletePage() { + const router = useRouter(); + const data = MOCK_PAYMENT_RESULT; + + const formatKRW = (n: number) => `${n.toLocaleString('ko-KR')}원`; + + return ( +
    +
    + {/* 아이콘 */} +
    +
    + success +
    +
    + + {/* 타이틀 */} +
    +

    + 스터디 수강 신청이 완료되었습니다. +

    +

    + 수강/학습 내역과 결제 내역은 마이페이지에서 확인하실 수 있습니다. +

    +
    + + {/* 정보 카드 */} +
    +
    + + +
    결제 정보
    + + + +
    + + + + +
    + + {/* 버튼 */} +
    + + + +
    +
    +
    +
    + ); +} + +function Row({ + label, + value, + bold, + strong, +}: { + label: string; + value: string; + bold?: boolean; + strong?: boolean; +}) { + return ( +
    +
    + {label} +
    +
    + {value} +
    +
    + ); +} diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx index 7e4bdd06..bc3bc409 100644 --- a/src/app/(service)/study/page.tsx +++ b/src/app/(service)/study/page.tsx @@ -1,3 +1,6 @@ +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import { Configuration } from '@/api/openapi/configuration'; +import { GroupStudyFullResponseDto } from '@/api/openapi/models'; import Button from '@/components/ui/button'; import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; import GroupStudyList from '@/features/study/group/ui/group-study-list'; @@ -5,10 +8,25 @@ import IconPlus from '@/shared/icons/plus.svg'; import { getServerCookie } from '@/utils/server-cookie'; import Sidebar from '@/widgets/home/sidebar'; +interface GroupStudyResponse { + content?: GroupStudyFullResponseDto; +} + export default async function Study() { const memberIdStr = await getServerCookie('memberId'); const isLoggedIn = !!memberIdStr; + // const config = new Configuration({ + // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, + // }); + + // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); + + // const response = await groupStudyApi.getGroupStudies(); + // const groupStudy = (response.data as GroupStudyResponse)?.content; + + // console.log('groupStudy', groupStudy); + return (
    diff --git a/src/components/payment/orderSummary.tsx b/src/components/payment/orderSummary.tsx new file mode 100644 index 00000000..4a9cf04f --- /dev/null +++ b/src/components/payment/orderSummary.tsx @@ -0,0 +1,27 @@ +interface Props { + study: { + title: string; + desc: string; + price: number; + thumbnailUrl?: string; + }; +} + +export default function OrderSummary({ study }: Props) { + return ( +
    +
    +
    +
    +
    +

    {study.title}

    +

    {study.desc}

    +
    +

    + {study.price.toLocaleString()}원 +

    +
    +
    +
    + ); +} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx new file mode 100644 index 00000000..77a77253 --- /dev/null +++ b/src/components/payment/paymentActionClient.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; +import { useEffect, useMemo, useState } from 'react'; +import { cn } from '../ui/(shadcn)/lib/utils'; +import Button from '../ui/button'; +import Checkbox from '../ui/checkbox'; +import { RadioGroup, RadioGroupItem } from '../ui/radio'; + +interface Props { + orderId: string; + amount: number; +} + +type PaymentMethod = 'CARD' | 'VBANK'; +const clientKey = 'test_ck_ORzdMaqN3wEbO04g0xNNr5AkYXQG'; +const customerKey = 'TwG7MSXwcuFlMaug2sHpf'; + +const methods: { id: PaymentMethod; label: string }[] = [ + { id: 'CARD', label: '신용카드 결제' }, + { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, +]; + +export default function PaymentCheckoutPage({ + orderId, + // amount, +}: Props) { + const [payment, setPayment] = useState(null); + const [isAgreed, setIsAgreed] = useState(false); + + const [errorMsg, setErrorMsg] = useState(null); + + const [paymentMethod, setPaymentMethod] = useState('CARD'); + + const canPay = isAgreed && !!paymentMethod; + + const toggleTerm = () => { + setIsAgreed((prev) => !prev); + }; + + const onPay = async () => { + setErrorMsg(null); + + if (!isAgreed) { + setErrorMsg('필수 약관에 동의해 주세요.'); + + return; + } + + if (!paymentMethod) { + setErrorMsg('결제 수단을 선택해 주세요.'); + + return; + } + + try { + await payment.requestPayment({ + method: 'CARD', // 카드 및 간편결제 + amount: 50000, // 결제 금액 + orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 + orderName: '토스 티셔츠 외 2건', + successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL + failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + customerEmail: 'customer123@gmail.com', + customerName: '김토스', + customerMobilePhone: '01012341234', + // 카드 결제에 필요한 정보 + card: { + useEscrow: false, + flowMode: 'DEFAULT', // 통합결제창 여는 옵션 + useCardPoint: false, + useAppCardOnly: false, + }, + }); + + // if (!res.ok) throw new Error('PAYMENT_SESSION_FAILED'); + + // const data: { redirectUrl: string } = await res.json(); + // window.location.href = data.redirectUrl; + } catch { + setErrorMsg('결제 요청에 실패했어요. 잠시 후 다시 시도해 주세요.'); + } finally { + } + }; + + const [amount] = useState({ + currency: 'KRW', + value: 50000, + }); + + useEffect(() => { + async function fetchPayment() { + try { + const tossPayments = await loadTossPayments(clientKey); + + // 회원 결제 + // @docs https://docs.tosspayments.com/sdk/v2/js#tosspaymentspayment + const payment = tossPayments.payment({ + customerKey, + }); + + setPayment(payment); + } catch (error) { + console.error('Error fetching payment:', error); + } + } + + fetchPayment().catch((error) => { + console.error('Error in fetchPayment:', error); + }); + }, [clientKey, customerKey]); + + return ( +
    + {/* 이용약관 */} +
    +
    +
    + + + 이용약관 동의 (필수) + +
    + + + 내용보기 + +
    +
    + + {/* 결제수단 */} +
    +

    결제 수단

    + + setPaymentMethod(v as PaymentMethod)} + className="space-y-200" + > + {methods.map((m) => { + const selected = paymentMethod === m.id; + + return ( + + ); + })} + + + +
    +
    + ); +} diff --git a/src/components/payment/priceSummary.tsx b/src/components/payment/priceSummary.tsx new file mode 100644 index 00000000..07c32572 --- /dev/null +++ b/src/components/payment/priceSummary.tsx @@ -0,0 +1,24 @@ +// app/checkout/_components/PriceSummary.tsx +interface Props { + price: number; +} + +export default function PriceSummary({ price }: Props) { + return ( +
    +
    +

    상품 금액

    +

    + {price.toLocaleString()}원 +

    +
    + +
    + +
    +

    총 결제 금액

    +

    {price.toLocaleString()}원

    +
    +
    + ); +} diff --git a/src/components/ui/radio/index.tsx b/src/components/ui/radio/index.tsx index 9e45c113..d0eeb133 100644 --- a/src/components/ui/radio/index.tsx +++ b/src/components/ui/radio/index.tsx @@ -19,7 +19,7 @@ function RadioGroup({ } const radioGroupItemVariants = cva( - 'border-border-default hover:bg-fill-neutral-subtle-hover disabled:border-border-disabled disabled:bg-icon-disabled data-[state=checked]:border-border-success rounded-full border-2 bg-white data-[state=checked]:border-4', + 'border-border-default hover:bg-fill-neutral-subtle-hover disabled:border-border-disabled disabled:bg-icon-disabled data-[state=checked]:border-border-success rounded-full border-2 bg-[#fff] data-[state=checked]:border-4', { variants: { size: { @@ -45,7 +45,7 @@ function RadioGroupItem({ className={cn(radioGroupItemVariants({ size }), className)} {...props} > - + ); } diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 846cd85d..6f355e81 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -149,7 +149,7 @@ export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { ))}
    - + /> */}
    ); })} diff --git a/yarn.lock b/yarn.lock index 33d39834..c93995c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2838,6 +2838,11 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== +"@tosspayments/tosspayments-sdk@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@tosspayments/tosspayments-sdk/-/tosspayments-sdk-2.5.0.tgz#2ebc019be3db092e6603f1ba42edf3d9655b183a" + integrity sha512-qapms+cTY5/4MBwcEegG1GkYBIWVjWv77yWnG2+CWXqdZOtGKLmqt0pb1lbrevBOF/uhO1f2Wtj+r/bggHwiHQ== + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" From e805b1e411667998946378fde94ce8ee8e40cc33 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 27 Dec 2025 00:04:40 +0900 Subject: [PATCH 004/211] =?UTF-8?q?=EC=9C=A0=EB=A3=8C=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client/open-api-instance.ts | 10 + src/app/(service)/(my)/my-study/page.tsx | 2 + .../{study => group-study}/[id]/page.tsx | 0 src/app/(service)/group-study/page.tsx | 65 ++++ src/app/(service)/payment/[id]/page.tsx | 67 +--- src/app/(service)/payment/fail/page.tsx | 99 ++++++ src/app/(service)/payment/success/page.tsx | 171 +++++++++ src/app/(service)/premium-study/[id]/page.tsx | 112 ++++++ src/app/(service)/premium-study/page.tsx | 65 ++++ src/app/(service)/study/page.tsx | 56 --- src/components/payment/PaymentTermsModal.tsx | 116 +++++++ .../payment/paymentActionClient.tsx | 71 ++-- .../my-page/ui/my-study-info-card.tsx | 2 +- .../group/api/get-group-study-list.server.ts | 26 ++ .../study/group/api/get-group-study-list.ts | 3 +- .../group/model/group-study-form.schema.ts | 17 +- .../group/model/use-group-study-list-query.ts | 1 + .../study/group/ui/group-study-form-modal.tsx | 8 +- .../study/group/ui/group-study-form.tsx | 14 +- .../study/group/ui/group-study-list.tsx | 300 +++++++--------- .../study/group/ui/group-study-pagination.tsx | 32 ++ .../study/group/ui/step/step1-group.tsx | 38 +- .../premium/ui/premium-study-detail-page.tsx | 238 +++++++++++++ .../premium/ui/premium-study-info-section.tsx | 326 ++++++++++++++++++ .../study/premium/ui/premium-study-list.tsx | 159 +++++++++ .../premium/ui/premium-study-pagination.tsx | 32 ++ .../premium/ui/premium-summary-study-info.tsx | 144 ++++++++ src/widgets/home/header.tsx | 4 +- 28 files changed, 1837 insertions(+), 341 deletions(-) rename src/app/(service)/{study => group-study}/[id]/page.tsx (100%) create mode 100644 src/app/(service)/group-study/page.tsx create mode 100644 src/app/(service)/payment/fail/page.tsx create mode 100644 src/app/(service)/payment/success/page.tsx create mode 100644 src/app/(service)/premium-study/[id]/page.tsx create mode 100644 src/app/(service)/premium-study/page.tsx delete mode 100644 src/app/(service)/study/page.tsx create mode 100644 src/components/payment/PaymentTermsModal.tsx create mode 100644 src/features/study/group/api/get-group-study-list.server.ts create mode 100644 src/features/study/group/ui/group-study-pagination.tsx create mode 100644 src/features/study/premium/ui/premium-study-detail-page.tsx create mode 100644 src/features/study/premium/ui/premium-study-info-section.tsx create mode 100644 src/features/study/premium/ui/premium-study-list.tsx create mode 100644 src/features/study/premium/ui/premium-study-pagination.tsx create mode 100644 src/features/study/premium/ui/premium-summary-study-info.tsx diff --git a/src/api/client/open-api-instance.ts b/src/api/client/open-api-instance.ts index 644042af..04bae77b 100644 --- a/src/api/client/open-api-instance.ts +++ b/src/api/client/open-api-instance.ts @@ -22,3 +22,13 @@ export const createApiInstance = ( ): T => { return new ApiClass(openapiConfig, openapiConfig.basePath, axiosInstanceV2); }; + +export const createApiServerInstance = ( + ApiClass: new ( + config: Configuration, + basePath?: string, + axios?: AxiosInstance, + ) => T, +): T => { + return new ApiClass(openapiConfig, openapiConfig.basePath, axiosInstanceV2); +}; diff --git a/src/app/(service)/(my)/my-study/page.tsx b/src/app/(service)/(my)/my-study/page.tsx index d728059d..3cd11c4f 100644 --- a/src/app/(service)/(my)/my-study/page.tsx +++ b/src/app/(service)/(my)/my-study/page.tsx @@ -17,6 +17,8 @@ interface MemberGroupStudyList extends MemberStudyItem { export default function MyStudy() { const { data: authData } = useAuth(); + console.log('data', authData); + const { data, isLoading } = useMemberStudyListQuery({ memberId: authData?.memberId, studyType: 'GROUP_STUDY', diff --git a/src/app/(service)/study/[id]/page.tsx b/src/app/(service)/group-study/[id]/page.tsx similarity index 100% rename from src/app/(service)/study/[id]/page.tsx rename to src/app/(service)/group-study/[id]/page.tsx diff --git a/src/app/(service)/group-study/page.tsx b/src/app/(service)/group-study/page.tsx new file mode 100644 index 00000000..3198ea93 --- /dev/null +++ b/src/app/(service)/group-study/page.tsx @@ -0,0 +1,65 @@ +import { Play, Plus } from 'lucide-react'; +import { createApiServerInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import Button from '@/components/ui/button'; +import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; +import GroupStudyList from '@/features/study/group/ui/group-study-list'; +import GroupStudyPagination from '@/features/study/group/ui/group-study-pagination'; + +interface GroupStudyPageProps { + searchParams: Promise<{ page?: string }>; +} + +export default async function GroupStudyPage({ + searchParams, +}: GroupStudyPageProps) { + const params = await searchParams; + const currentPage = Number(params.page) || 1; + const pageSize = 9; + + const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudies( + 'GROUP_STUDY', + currentPage, + pageSize, + ); + + console.log('data', data); + + return ( +
    + {/* 헤더 */} +
    +

    + 그룹스터디 둘러보기 +

    + } + iconPosition="left" + > + 스터디 개설하기 + + } + /> +
    + + {/* 스터디 카드 그리드 */} + + + {/* 페이지네이션 */} + {data.totalPages > 1 && ( + + )} +
    + ); +} diff --git a/src/app/(service)/payment/[id]/page.tsx b/src/app/(service)/payment/[id]/page.tsx index a51a8d69..1f4a76a2 100644 --- a/src/app/(service)/payment/[id]/page.tsx +++ b/src/app/(service)/payment/[id]/page.tsx @@ -2,8 +2,8 @@ import OrderSummary from '@/components/payment/orderSummary'; import PaymentCheckoutPage from '@/components/payment/paymentActionClient'; -import CheckoutActionClient from '@/components/payment/paymentActionClient'; import PriceSummary from '@/components/payment/priceSummary'; +import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; interface Study { id: string; @@ -13,58 +13,26 @@ interface Study { thumbnailUrl?: string; } -interface Terms { - id: string; - label: string; - required: boolean; - url?: string; +interface PaymentPageProps { + params: Promise<{ id: string }>; } -interface PaymentMethod { - id: 'CARD' | 'VBANK'; - label: string; - subLabel?: string; -} +async function getStudyData(groupStudyId: number): Promise { + const studyDetail = await getGroupStudyDetailInServer({ groupStudyId }); -async function getCheckoutData(): Promise<{ - study: Study; - terms: Terms[]; - methods: PaymentMethod[]; -}> { - // 실제론 db / internal api에서 가져오기 return { - study: { - id: 'study_1', - title: '1일1코테류프를 인증 챌린지', - desc: '1인 개발자, 이제는 대세가 되었다죠.', - price: 35000, - thumbnailUrl: '', - }, - terms: [ - { - id: 'terms_usage', - label: '이용약관 동의 (필수)', - required: true, - url: '/terms/usage', - }, - ], - methods: [ - { id: 'CARD', label: '신용카드 결제' }, - { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, - ], + id: String(studyDetail.basicInfo.groupStudyId), + title: studyDetail.detailInfo.title, + desc: studyDetail.detailInfo.summary, + price: studyDetail.basicInfo.price, + thumbnailUrl: + studyDetail.detailInfo.image?.resizedImages?.[0]?.resizedImageUrl || '', }; } -export default async function CheckoutPage() { - const { study, terms, methods } = await getCheckoutData(); - - // const clientKey = 'test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm'; - // const tossPayments = await loadTossPayments(clientKey); - - // const customerKey = "pfROxUh11lHWaPrBxzIBN"; - // const widgets = tossPayments.widgets({ - // customerKey, - // }); +export default async function CheckoutPage({ params }: PaymentPageProps) { + const { id } = await params; + const study = await getStudyData(Number(id)); return (
    @@ -88,12 +56,7 @@ export default async function CheckoutPage() { {/* 클라 렌더: 약관/결제수단/결제하기 */}
    - +
    diff --git a/src/app/(service)/payment/fail/page.tsx b/src/app/(service)/payment/fail/page.tsx new file mode 100644 index 00000000..2fa5c423 --- /dev/null +++ b/src/app/(service)/payment/fail/page.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { useSearchParams, useRouter } from 'next/navigation'; +import Button from '@/components/ui/button'; + +// 토스페이먼츠 에러 코드별 사용자 친화적 메시지 +const ERROR_MESSAGES: Record = { + PAY_PROCESS_CANCELED: '결제가 취소되었습니다.', + PAY_PROCESS_ABORTED: '결제가 중단되었습니다.', + REJECT_CARD_COMPANY: '카드사에서 결제를 거절했습니다.', + INVALID_CARD_EXPIRATION: '카드 유효기간이 만료되었습니다.', + INVALID_STOPPED_CARD: '정지된 카드입니다.', + INVALID_CARD_LOST: '분실 신고된 카드입니다.', + INVALID_CARD_NUMBER: '카드 번호가 올바르지 않습니다.', + EXCEED_MAX_DAILY_PAYMENT_COUNT: '일일 결제 한도를 초과했습니다.', + EXCEED_MAX_PAYMENT_AMOUNT: '결제 금액 한도를 초과했습니다.', + NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT: + '할부가 지원되지 않는 카드입니다.', + INVALID_CARD_INSTALLMENT_PLAN: '할부 개월 수가 올바르지 않습니다.', + NOT_ALLOWED_POINT_USE: '포인트 사용이 불가한 카드입니다.', + INVALID_API_KEY: '결제 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + INVALID_ORDER_ID: '주문 정보가 올바르지 않습니다.', + INVALID_AMOUNT: '결제 금액이 올바르지 않습니다.', +}; + +function getErrorMessage(code: string | null, message: string | null): string { + if (code && ERROR_MESSAGES[code]) { + return ERROR_MESSAGES[code]; + } + + if (message) { + return message; + } + + return '결제 중 오류가 발생했습니다. 다시 시도해주세요.'; +} + +export default function PaymentFailPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const code = searchParams.get('code'); + const message = searchParams.get('message'); + const orderId = searchParams.get('orderId'); + + const errorMessage = getErrorMessage(code, message); + + return ( +
    +
    +
    + +
    +

    결제에 실패했습니다

    +

    {errorMessage}

    + + {(code || orderId) && ( +
    + {orderId && ( +
    + 주문번호 + {orderId} +
    + )} + {code && ( +
    + 에러코드 + + {code} + +
    + )} +
    + )} + +
    + + +
    +
    +
    + ); +} diff --git a/src/app/(service)/payment/success/page.tsx b/src/app/(service)/payment/success/page.tsx new file mode 100644 index 00000000..5491d15f --- /dev/null +++ b/src/app/(service)/payment/success/page.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useSearchParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import Button from '@/components/ui/button'; + +interface PaymentConfirmResponse { + success: boolean; + message?: string; + data?: { + orderId: string; + amount: number; + paymentKey: string; + }; +} + +// TODO: 백엔드 API 호출 함수 - 실제 API 엔드포인트로 교체 필요 +async function confirmPayment( + paymentKey: string, + orderId: string, + amount: number, +): Promise { + // TODO: 실제 백엔드 API로 교체 + // const response = await fetch('/api/payment/confirm', { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ paymentKey, orderId, amount }), + // }); + // return response.json(); + + // 임시 성공 응답 + console.log('결제 승인 요청:', { paymentKey, orderId, amount }); + + return { + success: true, + data: { + orderId, + amount, + paymentKey, + }, + }; +} + +export default function PaymentSuccessPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const [status, setStatus] = useState<'loading' | 'success' | 'error'>( + 'loading', + ); + const [errorMessage, setErrorMessage] = useState(null); + + const paymentKey = searchParams.get('paymentKey'); + const orderId = searchParams.get('orderId'); + const amount = searchParams.get('amount'); + + useEffect(() => { + async function confirm() { + if (!paymentKey || !orderId || !amount) { + setStatus('error'); + setErrorMessage('결제 정보가 올바르지 않습니다.'); + + return; + } + + try { + const result = await confirmPayment( + paymentKey, + orderId, + Number(amount), + ); + + if (result.success) { + setStatus('success'); + } else { + setStatus('error'); + setErrorMessage(result.message || '결제 승인에 실패했습니다.'); + } + } catch { + setStatus('error'); + setErrorMessage('결제 승인 중 오류가 발생했습니다.'); + } + } + + confirm(); + }, [paymentKey, orderId, amount]); + + if (status === 'loading') { + return ( +
    +
    +
    +

    + 결제를 처리하고 있습니다... +

    +
    +
    + ); + } + + if (status === 'error') { + return ( +
    +
    +
    + +
    +

    결제 승인 실패

    +

    {errorMessage}

    + +
    +
    + ); + } + + return ( +
    +
    +
    + +
    +

    결제가 완료되었습니다

    +

    + 스터디 참여가 정상적으로 처리되었습니다. +

    + +
    +
    + 주문번호 + {orderId} +
    +
    + 결제금액 + + {Number(amount).toLocaleString()}원 + +
    +
    + +
    + + +
    +
    +
    + ); +} diff --git a/src/app/(service)/premium-study/[id]/page.tsx b/src/app/(service)/premium-study/[id]/page.tsx new file mode 100644 index 00000000..e7c8a752 --- /dev/null +++ b/src/app/(service)/premium-study/[id]/page.tsx @@ -0,0 +1,112 @@ +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; +import type { Metadata } from 'next'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import { Configuration } from '@/api/openapi/configuration'; +import type { GroupStudyFullResponseDto } from '@/api/openapi/models'; +import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; +import { getGroupStudyMyStatusInServer } from '@/features/study/group/api/get-group-study-my-status.server'; +import { GroupStudyDetailResponse } from '@/features/study/group/api/group-study-types'; +import PremiumStudyDetailPage from '@/features/study/premium/ui/premium-study-detail-page'; +import { getServerCookie } from '@/utils/server-cookie'; + +interface Props { + params: Promise<{ id: string }>; +} + +interface GroupStudyResponse { + content?: GroupStudyFullResponseDto; +} + +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + + try { + const config = new Configuration({ + basePath: process.env.NEXT_PUBLIC_API_BASE_URL, + }); + + const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); + + const response = await groupStudyApi.getGroupStudy(Number(id)); + const groupStudy = (response.data as GroupStudyResponse)?.content; + + if (!groupStudy) { + return { + title: '멘토 스터디 - 제로원', + description: '제로원 스터디 플랫폼에서 멘토 스터디를 둘러보세요.', + }; + } + + const title = groupStudy.detailInfo?.title || '멘토 스터디'; + const description = + groupStudy.detailInfo?.description || + groupStudy.detailInfo?.summary || + '제로원 멘토 스터디에 참여하세요.'; + + return { + title: `${title} - 제로원 멘토스터디`, + description, + openGraph: { + title: `${title} - 제로원 멘토스터디`, + description, + images: groupStudy.detailInfo?.image?.resizedImages?.[0] + ?.resizedImageUrl + ? [groupStudy.detailInfo.image.resizedImages[0].resizedImageUrl] + : [], + }, + }; + } catch { + return { + title: '멘토 스터디 - 제로원', + description: '제로원 스터디 플랫폼에서 멘토 스터디를 둘러보세요.', + }; + } +} + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + const queryClient = new QueryClient(); + + // 프리미엄 스터디 상세 정보 미리 가져오기 + await queryClient.fetchQuery({ + queryKey: ['groupStudyDetail', Number(id)], + queryFn: () => getGroupStudyDetailInServer({ groupStudyId: Number(id) }), + }); + + const data: GroupStudyDetailResponse = queryClient.getQueryData([ + 'groupStudyDetail', + Number(id), + ]); + + const memberIdStr = await getServerCookie('memberId'); + const memberId = memberIdStr ? Number(memberIdStr) : undefined; + + const isLeader = data.basicInfo.leader.memberId === memberId; + + if (!isLeader && memberId) { + // 내가 리더가 아닐 경우에만 내 신청 상태 정보 미리 가져오기 + await queryClient.prefetchQuery({ + queryKey: ['groupStudyMyStatus', Number(id)], + queryFn: () => + getGroupStudyMyStatusInServer({ groupStudyId: Number(id) }), + }); + } + + return ( + + + + ); +} diff --git a/src/app/(service)/premium-study/page.tsx b/src/app/(service)/premium-study/page.tsx new file mode 100644 index 00000000..b2b8ecfc --- /dev/null +++ b/src/app/(service)/premium-study/page.tsx @@ -0,0 +1,65 @@ +import { Plus } from 'lucide-react'; +import { createApiServerInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import Button from '@/components/ui/button'; +import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; +import PremiumStudyList from '@/features/study/premium/ui/premium-study-list'; +import PremiumStudyPagination from '@/features/study/premium/ui/premium-study-pagination'; + +interface PremiumStudyPageProps { + searchParams: Promise<{ page?: string }>; +} + +export default async function PremiumStudyPage({ + searchParams, +}: PremiumStudyPageProps) { + const params = await searchParams; + const currentPage = Number(params.page) || 1; + const pageSize = 9; + + const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudies( + 'PREMIUM_STUDY', + currentPage, + pageSize, + ); + + console.log('data', data); + + return ( +
    + {/* 헤더 */} +
    +

    + 멘토스터디 둘러보기 +

    + } + iconPosition="left" + > + 스터디 개설하기 + + } + /> +
    + + {/* 스터디 카드 그리드 */} + + + {/* 페이지네이션 */} + {data.totalPages > 1 && ( + + )} +
    + ); +} diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx deleted file mode 100644 index bc3bc409..00000000 --- a/src/app/(service)/study/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; -import { Configuration } from '@/api/openapi/configuration'; -import { GroupStudyFullResponseDto } from '@/api/openapi/models'; -import Button from '@/components/ui/button'; -import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; -import GroupStudyList from '@/features/study/group/ui/group-study-list'; -import IconPlus from '@/shared/icons/plus.svg'; -import { getServerCookie } from '@/utils/server-cookie'; -import Sidebar from '@/widgets/home/sidebar'; - -interface GroupStudyResponse { - content?: GroupStudyFullResponseDto; -} - -export default async function Study() { - const memberIdStr = await getServerCookie('memberId'); - const isLoggedIn = !!memberIdStr; - - // const config = new Configuration({ - // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, - // }); - - // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); - - // const response = await groupStudyApi.getGroupStudies(); - // const groupStudy = (response.data as GroupStudyResponse)?.content; - - // console.log('groupStudy', groupStudy); - - return ( -
    -
    -
    - - 스터디 둘러보기 - - } - > - 스터디 개설하기 - - } - /> -
    - -
    - {isLoggedIn && } -
    - ); -} diff --git a/src/components/payment/PaymentTermsModal.tsx b/src/components/payment/PaymentTermsModal.tsx new file mode 100644 index 00000000..dedcf478 --- /dev/null +++ b/src/components/payment/PaymentTermsModal.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { XIcon } from 'lucide-react'; +import Button from '../ui/button'; +import { Modal } from '../ui/modal'; + +export default function PaymentTermsModal() { + return ( + + + 내용보기 + + + + + + ZeroOne IT 서비스 이용약관 + + + + + +

    서비스 이용약관

    + +
    +

    1. 결제 및 서비스 제공

    +

    + 본 스터디 프로그램은 4주 과정의 온라인 학습 서비스이며, 결제 + 완료 시 즉시 서비스 이용이 가능하도록 구성됩니다. +

    +
    + +
    +

    2. 환불 가능 기간

    +

    + 서비스 특성상 다음과 같은 환불 기준을 적용합니다. +

    + +
    +
    +

    • 전액 환불

    +
      +
    • 결제일로부터 7일 이내
    • +
    • + 스터디 자료 열람 및 참여 이력이 0회일 경우 가능(예: 강의 + 시청, 자료 다운로드, 미션 수행, 커뮤니티 글 열람/참여 등) +
    • +
    +
    + +
    +

    • 부분 환불

    +
      +
    • + 아래 조건을 모두 충족할 경우 부분 환불이 가능합니다. +
    • +
    • 결제일로부터 7일 이내
    • +
    • 스터디 자료 사용 또는 참여 이력이 있을 경우
    • +
    • + 계산 방식 : 환불액 = 결제금액 - 총 금액 × (이용일수/총 + 스터디 일수) +
    • +
    • 콘텐츠/세션 이용 횟수에 따른 차감 후 환불
    • +
    +
    + +
    +

    • 환불 불가

    +
      +
    • 결제일로부터 7일 경과 후
    • +
    • 서비스 기간의 50% 이상 이용 시
    • +
    • + 스터디 자료 다운로드, 핵심 콘텐츠 접근, 참여 등으로 인해 + 회수 불가능한 가치 제공이 완료된 경우 +
    • +
    • + 회원의 과실, 단순 변심, 개인 일정 등의 사유로 서비스 이용이 + 어려운 경우 +
    • +
    +
    +
    +
    + +
    +

    3. 환불 접수 방법

    +
      +
    1. 환불 요청은 이메일 또는 채널톡/문의 폼을 통해 접수
    2. +
    3. + 환불 시 신분 확인을 위해 결제 정보 및 환불 계좌가 필요할 수 + 있음 +
    4. +
    5. 환불 처리는 접수 후 영업일 기준 3~5일 소요될 수 있음
    6. +
    +
    + +
    +

    4. 강제 퇴장/정책 위반

    +

    + 커뮤니티 규칙 위반, 불법 공유, 타인 비방 등 운영 정책을 위반한 + 경우 별도 환불 없이 서비스 이용이 제한될 수 있습니다. +

    +
    +
    + + + + + +
    +
    +
    + ); +} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx index 77a77253..c6e86371 100644 --- a/src/components/payment/paymentActionClient.tsx +++ b/src/components/payment/paymentActionClient.tsx @@ -1,15 +1,22 @@ 'use client'; import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; -import { useEffect, useMemo, useState } from 'react'; -import { cn } from '../ui/(shadcn)/lib/utils'; +import { useEffect, useState } from 'react'; import Button from '../ui/button'; import Checkbox from '../ui/checkbox'; import { RadioGroup, RadioGroupItem } from '../ui/radio'; +import PaymentTermsModal from './PaymentTermsModal'; + +interface Study { + id: string; + title: string; + desc: string; + price: number; + thumbnailUrl?: string; +} interface Props { - orderId: string; - amount: number; + study: Study; } type PaymentMethod = 'CARD' | 'VBANK'; @@ -21,10 +28,7 @@ const methods: { id: PaymentMethod; label: string }[] = [ { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, ]; -export default function PaymentCheckoutPage({ - orderId, - // amount, -}: Props) { +export default function PaymentCheckoutPage({ study }: Props) { const [payment, setPayment] = useState(null); const [isAgreed, setIsAgreed] = useState(false); @@ -55,19 +59,21 @@ export default function PaymentCheckoutPage({ try { await payment.requestPayment({ - method: 'CARD', // 카드 및 간편결제 - amount: 50000, // 결제 금액 - orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 - orderName: '토스 티셔츠 외 2건', - successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL - failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + method: paymentMethod, + amount: { + currency: 'KRW', + value: study.price, + }, + orderId: study.id, + orderName: study.title, + successUrl: window.location.origin + '/payment/success', + failUrl: window.location.origin + '/payment/fail', customerEmail: 'customer123@gmail.com', customerName: '김토스', customerMobilePhone: '01012341234', - // 카드 결제에 필요한 정보 card: { useEscrow: false, - flowMode: 'DEFAULT', // 통합결제창 여는 옵션 + flowMode: 'DEFAULT', useCardPoint: false, useAppCardOnly: false, }, @@ -83,10 +89,6 @@ export default function PaymentCheckoutPage({ } }; - const [amount] = useState({ - currency: 'KRW', - value: 50000, - }); useEffect(() => { async function fetchPayment() { @@ -127,14 +129,7 @@ export default function PaymentCheckoutPage({
    - - 내용보기 - +
    @@ -151,18 +146,18 @@ export default function PaymentCheckoutPage({ const selected = paymentMethod === m.id; return ( - + + {m.label} + ); })} diff --git a/src/features/my-page/ui/my-study-info-card.tsx b/src/features/my-page/ui/my-study-info-card.tsx index 5f5ee735..42eb540d 100644 --- a/src/features/my-page/ui/my-study-info-card.tsx +++ b/src/features/my-page/ui/my-study-info-card.tsx @@ -27,7 +27,7 @@ export default function MyStudyInfoCard({ return (
  • - +
    => { + const { page, size, status } = params; + + const { data } = await axiosServerInstance.get('/group-studies', { + params: { + page, + 'page-size': size, + groupStudyStatus: status, + }, + }); + + if (data.statusCode !== 200) { + throw new Error('Failed to fetch group study list'); + } + + return data.content; +}; diff --git a/src/features/study/group/api/get-group-study-list.ts b/src/features/study/group/api/get-group-study-list.ts index 4efe79c2..9fef5779 100644 --- a/src/features/study/group/api/get-group-study-list.ts +++ b/src/features/study/group/api/get-group-study-list.ts @@ -8,7 +8,7 @@ import { export const getGroupStudyList = async ( params: GroupStudyListRequest, ): Promise => { - const { page, size, status } = params; + const { page, size, status, classification } = params; try { const { data } = await axiosInstance.get('/group-studies', { @@ -16,6 +16,7 @@ export const getGroupStudyList = async ( page, 'page-size': size, groupStudyStatus: status, + classification: classification, }, }); diff --git a/src/features/study/group/model/group-study-form.schema.ts b/src/features/study/group/model/group-study-form.schema.ts index c05b650f..e3812a95 100644 --- a/src/features/study/group/model/group-study-form.schema.ts +++ b/src/features/study/group/model/group-study-form.schema.ts @@ -11,7 +11,11 @@ import { const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; +export const STUDY_CLASSIFICATION = ['GROUP_STUDY', 'PREMIUM_STUDY'] as const; +export type StudyClassification = (typeof STUDY_CLASSIFICATION)[number]; + export const GroupStudyFormSchema = z.object({ + classification: z.enum(STUDY_CLASSIFICATION), type: z.enum(STUDY_TYPES), targetRoles: z .array(z.enum(TARGET_ROLE_OPTIONS)) @@ -60,8 +64,11 @@ export type GroupStudyFormValues = z.input & { }; export type OpenGroupParsedValues = z.output; -export function buildOpenGroupDefaultValues(): GroupStudyFormValues { +export function buildOpenGroupDefaultValues( + classification: StudyClassification = 'GROUP_STUDY', +): GroupStudyFormValues { return { + classification, type: 'PROJECT', targetRoles: [], maxMembersCount: '', @@ -80,9 +87,9 @@ export function buildOpenGroupDefaultValues(): GroupStudyFormValues { }; } -export function toOpenGroupRequest( - v: OpenGroupParsedValues, -): GroupStudyFormRequest { +export function toOpenGroupRequest(v: OpenGroupParsedValues): GroupStudyFormRequest { + const isPremiumStudy = v.classification === 'PREMIUM_STUDY'; + return { basicInfo: { type: v.type, @@ -94,7 +101,7 @@ export function toOpenGroupRequest( location: v.location.trim(), startDate: v.startDate.trim(), endDate: v.endDate.trim(), - price: Number(v.price), + price: isPremiumStudy ? Number(v.price) || 0 : 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, diff --git a/src/features/study/group/model/use-group-study-list-query.ts b/src/features/study/group/model/use-group-study-list-query.ts index 4b6b3c60..8a236c45 100644 --- a/src/features/study/group/model/use-group-study-list-query.ts +++ b/src/features/study/group/model/use-group-study-list-query.ts @@ -9,6 +9,7 @@ export const useGroupStudyListQuery = () => { page: pageParam, size: 20, status: 'RECRUITING', + classification: 'GROUP_STUDY', }); return response; diff --git a/src/features/study/group/ui/group-study-form-modal.tsx b/src/features/study/group/ui/group-study-form-modal.tsx index 4f6ef0ef..47748deb 100644 --- a/src/features/study/group/ui/group-study-form-modal.tsx +++ b/src/features/study/group/ui/group-study-form-modal.tsx @@ -19,16 +19,20 @@ import { import { buildOpenGroupDefaultValues, GroupStudyFormValues, + StudyClassification, toOpenGroupRequest, } from '../model/group-study-form.schema'; import { useGroupStudyDetailQuery } from '../model/use-study-query'; +export type { StudyClassification }; + interface GroupStudyModalProps { trigger?: React.ReactNode; open?: boolean; onOpenChange?: () => void; mode: 'create' | 'edit'; groupStudyId?: number; + classification?: StudyClassification; } export default function GroupStudyFormModal({ @@ -37,6 +41,7 @@ export default function GroupStudyFormModal({ open: controlledOpen = false, groupStudyId, onOpenChange: onControlledOpen, + classification = 'GROUP_STUDY', }: GroupStudyModalProps) { const qc = useQueryClient(); const [open, setOpen] = useState(false); @@ -81,6 +86,7 @@ export default function GroupStudyFormModal({ if (isLoading) return; return { + classification, type: value.basicInfo.type, targetRoles: value.basicInfo.targetRoles, maxMembersCount: value.basicInfo.maxMembersCount.toString(), @@ -213,7 +219,7 @@ export default function GroupStudyFormModal({ ('GROUP_STUDY'); +export const useClassification = () => useContext(ClassificationContext); + interface GroupStudyFormProps { defaultValues: GroupStudyFormValues; onSubmit: (values: GroupStudyFormValues) => void; @@ -27,6 +31,7 @@ const STEP_FIELDS: Record<1 | 2 | 3, (keyof GroupStudyFormValues)[]> = { 'regularMeeting', 'startDate', 'endDate', + 'price', ], 2: ['thumbnailExtension', 'title', 'description', 'summary'], 3: ['interviewPost'], @@ -42,7 +47,8 @@ export default function GroupStudyForm({ defaultValues: defaultValues, }); - const { handleSubmit, trigger, formState } = methods; + const { handleSubmit, trigger, formState, watch } = methods; + const classification = watch('classification'); const [step, setStep] = useState<1 | 2 | 3>(1); @@ -63,7 +69,7 @@ export default function GroupStudyForm({ }; return ( - <> + @@ -124,7 +130,7 @@ export default function GroupStudyForm({ )}
    - + ); } diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 6f355e81..431f3729 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -1,199 +1,159 @@ 'use client'; import { sendGTMEvent } from '@next/third-parties/google'; -import { Loader2 } from 'lucide-react'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; +import Link from 'next/link'; import Badge from '@/components/ui/badge'; -import { useIntersectionObserver } from '@/hooks/common/use-intersection-observer'; +import Button from '@/components/ui/button'; import { useAuth } from '@/hooks/use-auth'; import { hashValue } from '@/utils/hash'; -import { BasicInfoDetail } from '../api/group-study-types'; -import { - EXPERIENCE_LEVEL_LABELS, - REGULAR_MEETING_LABELS, - ROLE_LABELS, - STUDY_TYPE_LABELS, -} from '../const/group-study-const'; -import { useGroupStudyListQuery } from '../model/use-group-study-list-query'; +import { GroupStudyData } from '../api/group-study-types'; +import { REGULAR_MEETING_LABELS } from '../const/group-study-const'; interface GroupStudyListProps { - isLoggedIn: boolean; + studies: GroupStudyData[]; } -export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { - const router = useRouter(); +export default function GroupStudyList({ studies }: GroupStudyListProps) { const { data: authData } = useAuth(); - const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = - useGroupStudyListQuery(); - const groupStudyList = data?.pages.flatMap((page) => page.content) || []; - - // useIntersectionObserver 커스텀 훅 사용 - const sentinelRef = useIntersectionObserver( - async () => { - if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage(); - } - }, - { - rootMargin: '200px 0px', - enabled: !!hasNextPage, - }, - ); - - const basicInfoItems = (basicInfo: BasicInfoDetail) => { - const { - type, - targetRoles, - experienceLevels, - regularMeeting, - maxMembersCount, - price, - approvedCount, - } = basicInfo; - - // 타입 변환 - const typeLabel = STUDY_TYPE_LABELS[type]; - - // 역할 변환 - const targetRolesLabel = targetRoles - .map((role) => { - return ROLE_LABELS[role]; - }) - .join(', '); - - // 경력 변환 - const experienceLabel = - experienceLevels - .map((level) => { - return EXPERIENCE_LEVEL_LABELS[level]; - }) - .join(', ') || '무관'; - - // 정기모임 - const frequencyLabel = REGULAR_MEETING_LABELS[regularMeeting]; - - // 참가비 - const priceLabel = price === 0 ? '무료' : `${price.toLocaleString()}원`; - - return [ - { label: '유형', value: typeLabel }, - { label: '주제', value: targetRolesLabel }, - { label: '경력', value: experienceLabel }, - { label: '정기모임', value: frequencyLabel }, - { label: '모집인원', value: `${approvedCount}/${maxMembersCount}` }, - { label: '참가비', value: priceLabel }, - ]; + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'group_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); }; - if (isLoading) { + if (studies.length === 0) { return ( -
    - +
    + 현재 그룹 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    ); } return ( - <> - {groupStudyList.length > 0 ? ( - <> -
    - {groupStudyList.map((study, index) => { - return ( -
    { - sendGTMEvent({ - event: 'group_study_detail_view', - dl_timestamp: new Date().toISOString(), - ...(authData?.memberId && { - dl_member_id: hashValue(String(authData.memberId)), - }), - dl_study_id: String(study.basicInfo.groupStudyId), - dl_study_title: study.simpleDetailInfo.title, - }); - router.push(`study/${study.basicInfo.groupStudyId}`); - }} - > -
    -
    -
    - {study.basicInfo.hostType === 'ZEROONE' && ( - 제로원 스터디 - )} - - - {study.simpleDetailInfo.title} - -
    -

    - {study.simpleDetailInfo.summary} -

    -
    -
    - {basicInfoItems(study.basicInfo).map((item, idx) => ( -
    - - {item.label} - - - {item.value} - -
    - ))} -
    -
    - {/* thumbnail */} -
    - ); - })} -
    - {/* 아래는 다음 페이지 데이터를 불러오기 위한 요소입니다. */} -
    } - className="h-10 w-full" - > - {isFetchingNextPage && ( -
    - +
    + {studies.map((study) => ( + handleStudyClick(study)} + className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" + > + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} + {study.basicInfo.hostType === 'ZEROONE' && ( +
    + 제로원 스터디
    )}
    - - ) : ( -
    - 현재 그룹 스터디가 없습니다. -
    - - 스터디가 아직 준비되지 않았습니다. - - - 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. - + {/* 컨텐츠 영역 */} +
    + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 리더 정보 */} +
    +
    +
    + {/* {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} */} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    +
    + + {study.basicInfo.approvedCount}/ + {study.basicInfo.maxMembersCount}명 + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 가격 및 버튼 */} +
    + + {study.basicInfo.price === 0 + ? '무료' + : `${study.basicInfo.price.toLocaleString()}원`} + + +
    -
    - )} - + + ))} +
    ); } diff --git a/src/features/study/group/ui/group-study-pagination.tsx b/src/features/study/group/ui/group-study-pagination.tsx new file mode 100644 index 00000000..9219a6c6 --- /dev/null +++ b/src/features/study/group/ui/group-study-pagination.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import Pagination from '@/components/ui/pagination'; + +interface GroupStudyPaginationProps { + currentPage: number; + totalPages: number; +} + +export default function GroupStudyPagination({ + currentPage, + totalPages, +}: GroupStudyPaginationProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleChangePage = (page: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', String(page)); + router.push(`/group-study?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index 8c7508aa..600126ba 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -23,6 +23,7 @@ import { REGULAR_MEETING_LABELS, } from '../../const/group-study-const'; import { GroupStudyFormValues } from '../../model/group-study-form.schema'; +import { useClassification } from '../group-study-form'; const methodOptions = STUDY_METHODS.map((v) => ({ label: STUDY_METHOD_LABELS[v], @@ -37,6 +38,9 @@ const memberOptions = Array.from({ length: 20 }, (_, i) => { export default function Step1OpenGroupStudy() { const { control, formState, watch } = useFormContext(); + const classification = useClassification(); + const isPremiumStudy = classification === 'PREMIUM_STUDY'; + const { field: typeField } = useController({ name: 'type', control, @@ -233,17 +237,29 @@ export default function Step1OpenGroupStudy() { )}
    - {/* API에는 있는데 디자인에는 없음. 뭐지??? */} - {/* - name="price" - label="참가비" - helper="참가비가 있다면 입력해주세요. (0원 가능)" - direction="vertical" - size="medium" - required - > - - */} + {isPremiumStudy && ( + + name="price" + label="참가비" + helper="참가비가 있다면 입력해주세요. (0원 가능)" + direction="vertical" + size="medium" + > + ( + + )} + /> + + )} ); } diff --git a/src/features/study/premium/ui/premium-study-detail-page.tsx b/src/features/study/premium/ui/premium-study-detail-page.tsx new file mode 100644 index 00000000..81a7c42e --- /dev/null +++ b/src/features/study/premium/ui/premium-study-detail-page.tsx @@ -0,0 +1,238 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import MoreMenu from '@/components/ui/dropdown/more-menu'; +import Tabs from '@/components/ui/tabs'; +import { useLeaderStore } from '@/stores/useLeaderStore'; +import ConfirmDeleteModal from '../../group/ui/confirm-delete-modal'; +import GroupStudyFormModal from '../../group/ui/group-study-form-modal'; +import GroupStudyMemberList from '../../group/ui/group-study-member-list'; +import PremiumStudyInfoSection from './premium-study-info-section'; +import ChannelSection from '../../group/channel/ui/channel-section'; +import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; +import { + useCompleteGroupStudyMutation, + useDeleteGroupStudyMutation, + useGroupStudyDetailQuery, +} from '../../group/model/use-study-query'; + +type ActiveTab = 'intro' | 'members' | 'channel'; + +type ActionKey = 'end' | 'delete'; + +interface PremiumStudyDetailPageProps { + groupStudyId: number; + memberId?: number; +} + +const TABS = [ + { label: '스터디 소개', value: 'intro' }, + { label: '참가자', value: 'members' }, + { label: '채널', value: 'channel' }, +]; + +export default function PremiumStudyDetailPage({ + groupStudyId, + memberId, +}: PremiumStudyDetailPageProps) { + const router = useRouter(); + + const { data: studyDetail, isLoading } = + useGroupStudyDetailQuery(groupStudyId); + + const leaderId = studyDetail?.basicInfo.leader.memberId; + + const isLeader = leaderId === memberId; + + const [active, setActive] = useState('intro'); + const [showModal, setShowModal] = useState(false); + const [action, setAction] = useState(null); + const [showStudyFormModal, setShowStudyFormModal] = useState(false); + + const setLeaderInfo = useLeaderStore((s) => s.setLeaderInfo); + + const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ + groupStudyId, + isLeader, + }); + + useEffect(() => { + const leader = studyDetail.basicInfo.leader; + setLeaderInfo(leader); + }, [studyDetail, setLeaderInfo]); + + const { mutate: deleteGroupStudy } = useDeleteGroupStudyMutation(); + const { mutate: completeStudy } = useCompleteGroupStudyMutation(); + + const ModalContent = { + end: { + title: '스터디를 종료하시겠어요?', + content: ( + <> + 종료 후에는 더 이상 모집/활동이 불가합니다. +
    이 동작은 되돌릴 수 없습니다. + + ), + confirmText: '스터디 종료', + onConfirm: () => { + completeStudy( + { groupStudyId }, + { + onSuccess: () => { + sendGTMEvent({ + event: 'premium_study_end', + group_study_id: String(groupStudyId), + }); + alert('스터디가 종료되었습니다.'); + }, + onSettled: () => { + setShowModal(false); + router.push('/premium-study'); + }, + }, + ); + }, + }, + delete: { + title: '스터디를 삭제하시겠어요?', + content: ( + <> + 삭제 시 모든 데이터가 영구적으로 제거됩니다. +
    이 동작은 되돌릴 수 없습니다. + + ), + confirmText: '스터디 삭제', + onConfirm: () => { + deleteGroupStudy( + { groupStudyId }, + { + onSuccess: () => { + sendGTMEvent({ + event: 'premium_study_delete', + group_study_id: String(groupStudyId), + }); + alert('스터디가 삭제되었습니다.'); + }, + onError: () => { + alert('스터디 삭제에 실패하였습니다.'); + }, + onSettled: () => { + router.push('/premium-study'); + setShowModal(false); + }, + }, + ); + }, + }, + }; + + // 참가자, 채널 탭 접근 가능 여부 = 스터디 참가자 또는 방장만 가능 + const isMember = + myApplicationStatus?.status === 'APPROVED' || + myApplicationStatus?.status === 'KICKED'; + + if (isLoading || !studyDetail) { + return
    로딩중...
    ; + } + + return ( +
    + setShowModal(!showModal)} + title={ModalContent[action]?.title} + content={ModalContent[action]?.content} + confirmText={ModalContent[action]?.confirmText} + onConfirm={ModalContent[action]?.onConfirm} + /> + setShowStudyFormModal(!showStudyFormModal)} + /> + +
    +
    +

    + {studyDetail?.detailInfo.title} +

    +

    + {studyDetail?.detailInfo.summary} +

    +
    + {memberId === studyDetail.basicInfo.leader.memberId && ( + { + setShowStudyFormModal(true); + }, + }, + { + label: '스터디 종료', + value: 'end', + onMenuClick: () => { + setAction('end'); + setShowModal(true); + }, + }, + { + label: '스터디 삭제', + value: 'delete', + onMenuClick: () => { + setAction('delete'); + setShowModal(true); + }, + }, + ]} + iconSize={35} + /> + )} +
    + + {/** 탭리스트 */} + tab.value === 'intro' || isLeader || isMember, + )} + activeTab={active} + onChange={(value: ActiveTab) => { + setActive(value); + sendGTMEvent({ + event: 'premium_study_tab_change', + group_study_id: String(groupStudyId), + tab: value, + }); + }} + /> + {active === 'intro' && ( + + )} + {active === 'members' && ( + + )} + {active === 'channel' && ( + + )} +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-info-section.tsx b/src/features/study/premium/ui/premium-study-info-section.tsx new file mode 100644 index 00000000..6066407a --- /dev/null +++ b/src/features/study/premium/ui/premium-study-info-section.tsx @@ -0,0 +1,326 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import dayjs from 'dayjs'; +import { + Calendar, + Clock, + File, + Folder, + Globe, + HandCoins, + MapPin, + SignpostBig, + UserCheck, + Users, +} from 'lucide-react'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import React from 'react'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import UserAvatar from '@/components/ui/avatar'; +import Button from '@/components/ui/button'; +import { getSincerityPresetByLevelName } from '@/config/sincerity-temp-presets'; +import UserProfileModal from '@/entities/user/ui/user-profile-modal'; +import { useAuth } from '@/hooks/use-auth'; +import { hashValue } from '@/utils/hash'; +import InfoCard from '@/widgets/study/group/ui/group-detail/info-card'; +import PremiumSummaryStudyInfo from './premium-summary-study-info'; + +import { + BasicInfoDetail, + GroupStudyDetailResponse, +} from '../../group/api/group-study-types'; + +import { useApplicantsByStatusQuery } from '../../group/application/model/use-applicant-qeury'; +import { + EXPERIENCE_LEVEL_LABELS, + REGULAR_MEETING_LABELS, + ROLE_LABELS, + STUDY_METHOD_LABELS, + STUDY_STATUS_LABELS, + STUDY_TYPE_LABELS, +} from '../../group/const/group-study-const'; + +interface PremiumStudyInfoSectionProps { + study: GroupStudyDetailResponse; + groupStudyId: number; + isLeader: boolean; + memberId?: number; +} + +export default function PremiumStudyInfoSection({ + study: studyDetail, + groupStudyId, + isLeader, + memberId, +}: PremiumStudyInfoSectionProps) { + const router = useRouter(); + const { data: authData } = useAuth(); + + const { data: approvedApplicants } = useApplicantsByStatusQuery({ + groupStudyId, + status: 'APPROVED', + }); + const { data: pendingApplicants } = useApplicantsByStatusQuery({ + groupStudyId, + status: 'PENDING', + }); + + const applicants = [ + ...(approvedApplicants?.pages.flatMap(({ content }) => content) || []), + ...(pendingApplicants?.pages.flatMap(({ content }) => content) || []), + ]; + + const basicInfoItems = (basicInfo: BasicInfoDetail) => { + const getDurationText = (startDate: string, endDate: string): string => { + const start = new Date(startDate); + const end = new Date(endDate); + + const diffTime = end.getTime() - start.getTime(); + if (diffTime < 0) return '기간이 잘못되었습니다.'; + + const diffDays = diffTime / (1000 * 60 * 60 * 24); + const diffWeeks = diffDays / 7; + const diffMonths = diffDays / 30; + + return diffMonths < 1 + ? `약 ${Math.round(diffWeeks)}주` + : `약 ${Math.round(diffMonths)}개월`; + }; + + return [ + { + label: '유형', + value: STUDY_TYPE_LABELS[basicInfo.type], + icon: , + }, + { + label: '주제', + value: basicInfo.targetRoles + .map((role) => { + return ROLE_LABELS[role]; + }) + .join(', '), + icon: , + }, + { + label: '경력', + value: + basicInfo.experienceLevels + .map((level) => { + return EXPERIENCE_LEVEL_LABELS[level]; + }) + .join(', ') || '무관', + icon: , + }, + { + label: '진행 방식', + value: `${STUDY_METHOD_LABELS[basicInfo.method]}${basicInfo.location ? `, ${basicInfo.location}` : ''}`, + icon: , + }, + { + label: '진행 기간', + value: getDurationText(basicInfo.startDate, basicInfo.endDate), + icon: , + }, + { + label: '정기모임', + value: REGULAR_MEETING_LABELS[basicInfo.regularMeeting], + icon: , + }, + { + label: '모집인원', + value: `${basicInfo.maxMembersCount}명`, + icon: , + }, + { + label: '시작일자', + value: dayjs(basicInfo.startDate).format('YYYY.MM.DD'), + icon: , + }, + { + label: '참가비', + value: + basicInfo.price === 0 + ? '무료' + : `${basicInfo.price.toLocaleString()}원`, + icon: , + }, + { + label: '상태', + value: `${STUDY_STATUS_LABELS[basicInfo.status]}`, + icon: , + }, + ]; + }; + + const summaryBasicInfoItems = (basicInfo: BasicInfoDetail) => { + return [ + { + label: '주제', + value: basicInfo.targetRoles + .map((role) => { + return ROLE_LABELS[role]; + }) + .join(', '), + icon: , + }, + { + label: '정기모임', + value: `${REGULAR_MEETING_LABELS[basicInfo.regularMeeting]}, ${basicInfo.location}`, + icon: , + }, + { + label: '경력', + value: + basicInfo.experienceLevels + .map((level) => { + return EXPERIENCE_LEVEL_LABELS[level]; + }) + .join(', ') || '무관', + icon: , + }, + { + label: '모집인원', + value: `${basicInfo.maxMembersCount}명`, + icon: , + }, + ]; + }; + + return ( +
    +
    +
    + 썸네일 +
    + +
    +
    +

    스터디 소개

    +
    + {studyDetail?.detailInfo.description} +
    +
    +
    +

    기본 정보

    +
    + {basicInfoItems(studyDetail?.basicInfo).map((item) => { + return ( + + ); + })} +
    +
    +
    +
    +
    + 실시간 신청자 목록 + {`${studyDetail.basicInfo.approvedCount + studyDetail.basicInfo.pendingCount}명`} +
    + {isLeader && ( + + )} +
    + +
    + {applicants.map((data) => { + const temperPreset = getSincerityPresetByLevelName( + data.applicantInfo.sincerityTemp.levelName as string, + ); + + return ( +
    + +
    +
    +
    + {data.applicantInfo.memberNickname !== '' + ? data.applicantInfo.memberNickname + : '익명'} +
    + + {`${data.applicantInfo.sincerityTemp.temperature}`}℃ + +
    +
    + { + sendGTMEvent({ + event: 'premium_study_member_profile_click', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue( + String(authData.memberId), + ), + }), + dl_target_member_id: String( + data.applicantInfo.memberId, + ), + dl_group_study_id: String(groupStudyId), + }); + }} + > + 프로필 +
    + } + /> +
    + ); + })} +
    +
    +
    +
    + +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-list.tsx b/src/features/study/premium/ui/premium-study-list.tsx new file mode 100644 index 00000000..5c18244a --- /dev/null +++ b/src/features/study/premium/ui/premium-study-list.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import Image from 'next/image'; +import Link from 'next/link'; +import Badge from '@/components/ui/badge'; +import Button from '@/components/ui/button'; +import { useAuth } from '@/hooks/use-auth'; +import { hashValue } from '@/utils/hash'; + +import { GroupStudyData } from '../../group/api/group-study-types'; +import { REGULAR_MEETING_LABELS } from '../../group/const/group-study-const'; + +interface PremiumStudyListProps { + studies: GroupStudyData[]; +} + +export default function PremiumStudyList({ studies }: PremiumStudyListProps) { + const { data: authData } = useAuth(); + + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'premium_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); + }; + + if (studies.length === 0) { + return ( +
    + 현재 멘토 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    +
    + ); + } + + return ( +
    + {studies.map((study) => ( + handleStudyClick(study)} + className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" + > + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} + {study.basicInfo.hostType === 'ZEROONE' && ( +
    + 제로원 스터디 +
    + )} +
    + + {/* 컨텐츠 영역 */} +
    + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 리더 정보 */} +
    +
    +
    + {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    +
    + + {study.basicInfo.approvedCount}/ + {study.basicInfo.maxMembersCount}명 + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 가격 및 버튼 */} +
    + + {study.basicInfo.price === 0 + ? '무료' + : `${study.basicInfo.price.toLocaleString()}원`} + + +
    +
    + + ))} +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-pagination.tsx b/src/features/study/premium/ui/premium-study-pagination.tsx new file mode 100644 index 00000000..51851adb --- /dev/null +++ b/src/features/study/premium/ui/premium-study-pagination.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import Pagination from '@/components/ui/pagination'; + +interface PremiumStudyPaginationProps { + currentPage: number; + totalPages: number; +} + +export default function PremiumStudyPagination({ + currentPage, + totalPages, +}: PremiumStudyPaginationProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleChangePage = (page: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', String(page)); + router.push(`/premium-study?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/src/features/study/premium/ui/premium-summary-study-info.tsx b/src/features/study/premium/ui/premium-summary-study-info.tsx new file mode 100644 index 00000000..3fa717bc --- /dev/null +++ b/src/features/study/premium/ui/premium-summary-study-info.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import React from 'react'; +import Button from '@/components/ui/button'; +import { + GroupStudyDetailResponse, + GroupStudyStatus, +} from '../../group/api/group-study-types'; +import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; + +interface PremiumSummaryStudyInfoProps { + data: { + label: string; + value: string; + icon: React.ReactNode; + }[]; + title: string; + groupStudyId: number; + questions: GroupStudyDetailResponse['interviewPost']['interviewPost']; + isLeader: boolean; + groupStudyStatus: GroupStudyStatus; + approvedCount: GroupStudyDetailResponse['basicInfo']['approvedCount']; + maxMembersCount: GroupStudyDetailResponse['basicInfo']['maxMembersCount']; + price: number; + memberId?: number; +} + +export default function PremiumSummaryStudyInfo({ + data, + title, + groupStudyId, + isLeader, + groupStudyStatus, + approvedCount, + maxMembersCount, + price, + memberId, +}: PremiumSummaryStudyInfoProps) { + const router = useRouter(); + const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ + groupStudyId, + isLeader, + }); + + const isLoggedIn = typeof memberId === 'number'; + + const handleCopyURL = async () => { + await navigator.clipboard.writeText(window.location.href); + alert('스터디 링크가 복사되었습니다!'); + }; + + const handleApplyClick = () => { + // 결제 페이지로 이동 (groupStudyId를 전달) + router.push(`/payment/${groupStudyId}`); + }; + + const isApplyDisabled = + myApplicationStatus?.status !== 'NONE' || + groupStudyStatus === 'IN_PROGRESS' || + approvedCount >= maxMembersCount; + + const getButtonText = () => { + if ( + myApplicationStatus?.status === 'APPROVED' || + groupStudyStatus === 'IN_PROGRESS' + ) { + return '참여 중인 스터디'; + } + if (myApplicationStatus?.status === 'PENDING') { + return '승인 대기중'; + } + return '신청하기'; + }; + + return ( +
    +

    {title}

    +
    +
    + {data.map((item) => ( +
    +
    {item.icon}
    + + {item.value} + +
    + ))} +
    + + {/* 가격 표시 */} +
    + 참가비 + + {price === 0 ? '무료' : `${price.toLocaleString()}원`} + +
    + +
    + {!isLeader && isLoggedIn && ( + + )} + {!isLoggedIn && ( + + )} + + +
    +
    + ); +} diff --git a/src/widgets/home/header.tsx b/src/widgets/home/header.tsx index 77d5c42b..3bdfa740 100644 --- a/src/widgets/home/header.tsx +++ b/src/widgets/home/header.tsx @@ -45,8 +45,8 @@ export default async function Header() { {/* 1차 MVP에선 사용하지 않아 제외 */} From ad6e9895f9283d6e0f79316b8404b1a933f78b80 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 20 Dec 2025 23:17:18 +0900 Subject: [PATCH 005/211] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/app/(service)/payment/[id]/page.tsx | 102 ++++++++++ src/app/(service)/payment/complete/page.tsx | 125 ++++++++++++ src/app/(service)/study/page.tsx | 18 ++ src/components/payment/orderSummary.tsx | 27 +++ .../payment/paymentActionClient.tsx | 183 ++++++++++++++++++ src/components/payment/priceSummary.tsx | 24 +++ src/components/ui/radio/index.tsx | 4 +- .../study/group/ui/group-study-list.tsx | 4 +- yarn.lock | 5 + 10 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 src/app/(service)/payment/[id]/page.tsx create mode 100644 src/app/(service)/payment/complete/page.tsx create mode 100644 src/components/payment/orderSummary.tsx create mode 100644 src/components/payment/paymentActionClient.tsx create mode 100644 src/components/payment/priceSummary.tsx diff --git a/package.json b/package.json index 3e61c778..3aa4020e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.0.6", + "@tosspayments/tosspayments-sdk": "^2.5.0", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/app/(service)/payment/[id]/page.tsx b/src/app/(service)/payment/[id]/page.tsx new file mode 100644 index 00000000..a51a8d69 --- /dev/null +++ b/src/app/(service)/payment/[id]/page.tsx @@ -0,0 +1,102 @@ +// app/checkout/page.tsx + +import OrderSummary from '@/components/payment/orderSummary'; +import PaymentCheckoutPage from '@/components/payment/paymentActionClient'; +import CheckoutActionClient from '@/components/payment/paymentActionClient'; +import PriceSummary from '@/components/payment/priceSummary'; + +interface Study { + id: string; + title: string; + desc: string; + price: number; + thumbnailUrl?: string; +} + +interface Terms { + id: string; + label: string; + required: boolean; + url?: string; +} + +interface PaymentMethod { + id: 'CARD' | 'VBANK'; + label: string; + subLabel?: string; +} + +async function getCheckoutData(): Promise<{ + study: Study; + terms: Terms[]; + methods: PaymentMethod[]; +}> { + // 실제론 db / internal api에서 가져오기 + return { + study: { + id: 'study_1', + title: '1일1코테류프를 인증 챌린지', + desc: '1인 개발자, 이제는 대세가 되었다죠.', + price: 35000, + thumbnailUrl: '', + }, + terms: [ + { + id: 'terms_usage', + label: '이용약관 동의 (필수)', + required: true, + url: '/terms/usage', + }, + ], + methods: [ + { id: 'CARD', label: '신용카드 결제' }, + { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, + ], + }; +} + +export default async function CheckoutPage() { + const { study, terms, methods } = await getCheckoutData(); + + // const clientKey = 'test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm'; + // const tossPayments = await loadTossPayments(clientKey); + + // const customerKey = "pfROxUh11lHWaPrBxzIBN"; + // const widgets = tossPayments.widgets({ + // customerKey, + // }); + + return ( +
    +
    +
    + {/* 서버 렌더: 선택한 스터디 */} +
    +

    선택한 스터디

    + + +
    + + {/* 서버 렌더: 결제 금액 */} +
    +

    결제 금액

    + +
    + +
    +
    + + {/* 클라 렌더: 약관/결제수단/결제하기 */} +
    + +
    +
    +
    +
    + ); +} diff --git a/src/app/(service)/payment/complete/page.tsx b/src/app/(service)/payment/complete/page.tsx new file mode 100644 index 00000000..2d07c9ed --- /dev/null +++ b/src/app/(service)/payment/complete/page.tsx @@ -0,0 +1,125 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import Button from '@/components/ui/button'; + +interface PaymentResult { + orderName: string; + productAmount: number; + paymentMethod: string; + totalAmount: number; +} + +// ✅ 가데이터 +const MOCK_PAYMENT_RESULT: PaymentResult = { + orderName: '1일코테문풀 인증 챌린지', + productAmount: 35000, + paymentMethod: '카드결제', + totalAmount: 35000, +}; + +export default function PaymentCompletePage() { + const router = useRouter(); + const data = MOCK_PAYMENT_RESULT; + + const formatKRW = (n: number) => `${n.toLocaleString('ko-KR')}원`; + + return ( +
    +
    + {/* 아이콘 */} +
    +
    + success +
    +
    + + {/* 타이틀 */} +
    +

    + 스터디 수강 신청이 완료되었습니다. +

    +

    + 수강/학습 내역과 결제 내역은 마이페이지에서 확인하실 수 있습니다. +

    +
    + + {/* 정보 카드 */} +
    +
    + + +
    결제 정보
    + + + +
    + + + + +
    + + {/* 버튼 */} +
    + + + +
    +
    +
    +
    + ); +} + +function Row({ + label, + value, + bold, + strong, +}: { + label: string; + value: string; + bold?: boolean; + strong?: boolean; +}) { + return ( +
    +
    + {label} +
    +
    + {value} +
    +
    + ); +} diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx index 7e4bdd06..bc3bc409 100644 --- a/src/app/(service)/study/page.tsx +++ b/src/app/(service)/study/page.tsx @@ -1,3 +1,6 @@ +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import { Configuration } from '@/api/openapi/configuration'; +import { GroupStudyFullResponseDto } from '@/api/openapi/models'; import Button from '@/components/ui/button'; import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; import GroupStudyList from '@/features/study/group/ui/group-study-list'; @@ -5,10 +8,25 @@ import IconPlus from '@/shared/icons/plus.svg'; import { getServerCookie } from '@/utils/server-cookie'; import Sidebar from '@/widgets/home/sidebar'; +interface GroupStudyResponse { + content?: GroupStudyFullResponseDto; +} + export default async function Study() { const memberIdStr = await getServerCookie('memberId'); const isLoggedIn = !!memberIdStr; + // const config = new Configuration({ + // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, + // }); + + // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); + + // const response = await groupStudyApi.getGroupStudies(); + // const groupStudy = (response.data as GroupStudyResponse)?.content; + + // console.log('groupStudy', groupStudy); + return (
    diff --git a/src/components/payment/orderSummary.tsx b/src/components/payment/orderSummary.tsx new file mode 100644 index 00000000..4a9cf04f --- /dev/null +++ b/src/components/payment/orderSummary.tsx @@ -0,0 +1,27 @@ +interface Props { + study: { + title: string; + desc: string; + price: number; + thumbnailUrl?: string; + }; +} + +export default function OrderSummary({ study }: Props) { + return ( +
    +
    +
    +
    +
    +

    {study.title}

    +

    {study.desc}

    +
    +

    + {study.price.toLocaleString()}원 +

    +
    +
    +
    + ); +} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx new file mode 100644 index 00000000..77a77253 --- /dev/null +++ b/src/components/payment/paymentActionClient.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; +import { useEffect, useMemo, useState } from 'react'; +import { cn } from '../ui/(shadcn)/lib/utils'; +import Button from '../ui/button'; +import Checkbox from '../ui/checkbox'; +import { RadioGroup, RadioGroupItem } from '../ui/radio'; + +interface Props { + orderId: string; + amount: number; +} + +type PaymentMethod = 'CARD' | 'VBANK'; +const clientKey = 'test_ck_ORzdMaqN3wEbO04g0xNNr5AkYXQG'; +const customerKey = 'TwG7MSXwcuFlMaug2sHpf'; + +const methods: { id: PaymentMethod; label: string }[] = [ + { id: 'CARD', label: '신용카드 결제' }, + { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, +]; + +export default function PaymentCheckoutPage({ + orderId, + // amount, +}: Props) { + const [payment, setPayment] = useState(null); + const [isAgreed, setIsAgreed] = useState(false); + + const [errorMsg, setErrorMsg] = useState(null); + + const [paymentMethod, setPaymentMethod] = useState('CARD'); + + const canPay = isAgreed && !!paymentMethod; + + const toggleTerm = () => { + setIsAgreed((prev) => !prev); + }; + + const onPay = async () => { + setErrorMsg(null); + + if (!isAgreed) { + setErrorMsg('필수 약관에 동의해 주세요.'); + + return; + } + + if (!paymentMethod) { + setErrorMsg('결제 수단을 선택해 주세요.'); + + return; + } + + try { + await payment.requestPayment({ + method: 'CARD', // 카드 및 간편결제 + amount: 50000, // 결제 금액 + orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 + orderName: '토스 티셔츠 외 2건', + successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL + failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + customerEmail: 'customer123@gmail.com', + customerName: '김토스', + customerMobilePhone: '01012341234', + // 카드 결제에 필요한 정보 + card: { + useEscrow: false, + flowMode: 'DEFAULT', // 통합결제창 여는 옵션 + useCardPoint: false, + useAppCardOnly: false, + }, + }); + + // if (!res.ok) throw new Error('PAYMENT_SESSION_FAILED'); + + // const data: { redirectUrl: string } = await res.json(); + // window.location.href = data.redirectUrl; + } catch { + setErrorMsg('결제 요청에 실패했어요. 잠시 후 다시 시도해 주세요.'); + } finally { + } + }; + + const [amount] = useState({ + currency: 'KRW', + value: 50000, + }); + + useEffect(() => { + async function fetchPayment() { + try { + const tossPayments = await loadTossPayments(clientKey); + + // 회원 결제 + // @docs https://docs.tosspayments.com/sdk/v2/js#tosspaymentspayment + const payment = tossPayments.payment({ + customerKey, + }); + + setPayment(payment); + } catch (error) { + console.error('Error fetching payment:', error); + } + } + + fetchPayment().catch((error) => { + console.error('Error in fetchPayment:', error); + }); + }, [clientKey, customerKey]); + + return ( +
    + {/* 이용약관 */} +
    +
    +
    + + + 이용약관 동의 (필수) + +
    + + + 내용보기 + +
    +
    + + {/* 결제수단 */} +
    +

    결제 수단

    + + setPaymentMethod(v as PaymentMethod)} + className="space-y-200" + > + {methods.map((m) => { + const selected = paymentMethod === m.id; + + return ( + + ); + })} + + + +
    +
    + ); +} diff --git a/src/components/payment/priceSummary.tsx b/src/components/payment/priceSummary.tsx new file mode 100644 index 00000000..07c32572 --- /dev/null +++ b/src/components/payment/priceSummary.tsx @@ -0,0 +1,24 @@ +// app/checkout/_components/PriceSummary.tsx +interface Props { + price: number; +} + +export default function PriceSummary({ price }: Props) { + return ( +
    +
    +

    상품 금액

    +

    + {price.toLocaleString()}원 +

    +
    + +
    + +
    +

    총 결제 금액

    +

    {price.toLocaleString()}원

    +
    +
    + ); +} diff --git a/src/components/ui/radio/index.tsx b/src/components/ui/radio/index.tsx index 9e45c113..d0eeb133 100644 --- a/src/components/ui/radio/index.tsx +++ b/src/components/ui/radio/index.tsx @@ -19,7 +19,7 @@ function RadioGroup({ } const radioGroupItemVariants = cva( - 'border-border-default hover:bg-fill-neutral-subtle-hover disabled:border-border-disabled disabled:bg-icon-disabled data-[state=checked]:border-border-success rounded-full border-2 bg-white data-[state=checked]:border-4', + 'border-border-default hover:bg-fill-neutral-subtle-hover disabled:border-border-disabled disabled:bg-icon-disabled data-[state=checked]:border-border-success rounded-full border-2 bg-[#fff] data-[state=checked]:border-4', { variants: { size: { @@ -45,7 +45,7 @@ function RadioGroupItem({ className={cn(radioGroupItemVariants({ size }), className)} {...props} > - + ); } diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 846cd85d..6f355e81 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -149,7 +149,7 @@ export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { ))}
    - + /> */}
    ); })} diff --git a/yarn.lock b/yarn.lock index 33d39834..c93995c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2838,6 +2838,11 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== +"@tosspayments/tosspayments-sdk@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@tosspayments/tosspayments-sdk/-/tosspayments-sdk-2.5.0.tgz#2ebc019be3db092e6603f1ba42edf3d9655b183a" + integrity sha512-qapms+cTY5/4MBwcEegG1GkYBIWVjWv77yWnG2+CWXqdZOtGKLmqt0pb1lbrevBOF/uhO1f2Wtj+r/bggHwiHQ== + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" From 5040be1f962897873c615f7144df55f590051187 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 27 Dec 2025 00:04:40 +0900 Subject: [PATCH 006/211] =?UTF-8?q?=EC=9C=A0=EB=A3=8C=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client/open-api-instance.ts | 10 + src/app/(service)/(my)/my-study/page.tsx | 2 + .../{study => group-study}/[id]/page.tsx | 0 src/app/(service)/group-study/page.tsx | 65 ++++ src/app/(service)/payment/[id]/page.tsx | 67 +--- src/app/(service)/payment/fail/page.tsx | 99 ++++++ src/app/(service)/payment/success/page.tsx | 171 +++++++++ src/app/(service)/premium-study/[id]/page.tsx | 112 ++++++ src/app/(service)/premium-study/page.tsx | 65 ++++ src/app/(service)/study/page.tsx | 56 --- src/components/payment/PaymentTermsModal.tsx | 116 +++++++ .../payment/paymentActionClient.tsx | 71 ++-- .../my-page/ui/my-study-info-card.tsx | 2 +- .../group/api/get-group-study-list.server.ts | 26 ++ .../study/group/api/get-group-study-list.ts | 3 +- .../group/model/group-study-form.schema.ts | 17 +- .../group/model/use-group-study-list-query.ts | 1 + .../study/group/ui/group-study-form-modal.tsx | 8 +- .../study/group/ui/group-study-form.tsx | 14 +- .../study/group/ui/group-study-list.tsx | 300 +++++++--------- .../study/group/ui/group-study-pagination.tsx | 32 ++ .../study/group/ui/step/step1-group.tsx | 38 +- .../premium/ui/premium-study-detail-page.tsx | 238 +++++++++++++ .../premium/ui/premium-study-info-section.tsx | 326 ++++++++++++++++++ .../study/premium/ui/premium-study-list.tsx | 159 +++++++++ .../premium/ui/premium-study-pagination.tsx | 32 ++ .../premium/ui/premium-summary-study-info.tsx | 144 ++++++++ src/widgets/home/header.tsx | 4 +- 28 files changed, 1837 insertions(+), 341 deletions(-) rename src/app/(service)/{study => group-study}/[id]/page.tsx (100%) create mode 100644 src/app/(service)/group-study/page.tsx create mode 100644 src/app/(service)/payment/fail/page.tsx create mode 100644 src/app/(service)/payment/success/page.tsx create mode 100644 src/app/(service)/premium-study/[id]/page.tsx create mode 100644 src/app/(service)/premium-study/page.tsx delete mode 100644 src/app/(service)/study/page.tsx create mode 100644 src/components/payment/PaymentTermsModal.tsx create mode 100644 src/features/study/group/api/get-group-study-list.server.ts create mode 100644 src/features/study/group/ui/group-study-pagination.tsx create mode 100644 src/features/study/premium/ui/premium-study-detail-page.tsx create mode 100644 src/features/study/premium/ui/premium-study-info-section.tsx create mode 100644 src/features/study/premium/ui/premium-study-list.tsx create mode 100644 src/features/study/premium/ui/premium-study-pagination.tsx create mode 100644 src/features/study/premium/ui/premium-summary-study-info.tsx diff --git a/src/api/client/open-api-instance.ts b/src/api/client/open-api-instance.ts index 644042af..04bae77b 100644 --- a/src/api/client/open-api-instance.ts +++ b/src/api/client/open-api-instance.ts @@ -22,3 +22,13 @@ export const createApiInstance = ( ): T => { return new ApiClass(openapiConfig, openapiConfig.basePath, axiosInstanceV2); }; + +export const createApiServerInstance = ( + ApiClass: new ( + config: Configuration, + basePath?: string, + axios?: AxiosInstance, + ) => T, +): T => { + return new ApiClass(openapiConfig, openapiConfig.basePath, axiosInstanceV2); +}; diff --git a/src/app/(service)/(my)/my-study/page.tsx b/src/app/(service)/(my)/my-study/page.tsx index d728059d..3cd11c4f 100644 --- a/src/app/(service)/(my)/my-study/page.tsx +++ b/src/app/(service)/(my)/my-study/page.tsx @@ -17,6 +17,8 @@ interface MemberGroupStudyList extends MemberStudyItem { export default function MyStudy() { const { data: authData } = useAuth(); + console.log('data', authData); + const { data, isLoading } = useMemberStudyListQuery({ memberId: authData?.memberId, studyType: 'GROUP_STUDY', diff --git a/src/app/(service)/study/[id]/page.tsx b/src/app/(service)/group-study/[id]/page.tsx similarity index 100% rename from src/app/(service)/study/[id]/page.tsx rename to src/app/(service)/group-study/[id]/page.tsx diff --git a/src/app/(service)/group-study/page.tsx b/src/app/(service)/group-study/page.tsx new file mode 100644 index 00000000..3198ea93 --- /dev/null +++ b/src/app/(service)/group-study/page.tsx @@ -0,0 +1,65 @@ +import { Play, Plus } from 'lucide-react'; +import { createApiServerInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import Button from '@/components/ui/button'; +import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; +import GroupStudyList from '@/features/study/group/ui/group-study-list'; +import GroupStudyPagination from '@/features/study/group/ui/group-study-pagination'; + +interface GroupStudyPageProps { + searchParams: Promise<{ page?: string }>; +} + +export default async function GroupStudyPage({ + searchParams, +}: GroupStudyPageProps) { + const params = await searchParams; + const currentPage = Number(params.page) || 1; + const pageSize = 9; + + const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudies( + 'GROUP_STUDY', + currentPage, + pageSize, + ); + + console.log('data', data); + + return ( +
    + {/* 헤더 */} +
    +

    + 그룹스터디 둘러보기 +

    + } + iconPosition="left" + > + 스터디 개설하기 + + } + /> +
    + + {/* 스터디 카드 그리드 */} + + + {/* 페이지네이션 */} + {data.totalPages > 1 && ( + + )} +
    + ); +} diff --git a/src/app/(service)/payment/[id]/page.tsx b/src/app/(service)/payment/[id]/page.tsx index a51a8d69..1f4a76a2 100644 --- a/src/app/(service)/payment/[id]/page.tsx +++ b/src/app/(service)/payment/[id]/page.tsx @@ -2,8 +2,8 @@ import OrderSummary from '@/components/payment/orderSummary'; import PaymentCheckoutPage from '@/components/payment/paymentActionClient'; -import CheckoutActionClient from '@/components/payment/paymentActionClient'; import PriceSummary from '@/components/payment/priceSummary'; +import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; interface Study { id: string; @@ -13,58 +13,26 @@ interface Study { thumbnailUrl?: string; } -interface Terms { - id: string; - label: string; - required: boolean; - url?: string; +interface PaymentPageProps { + params: Promise<{ id: string }>; } -interface PaymentMethod { - id: 'CARD' | 'VBANK'; - label: string; - subLabel?: string; -} +async function getStudyData(groupStudyId: number): Promise { + const studyDetail = await getGroupStudyDetailInServer({ groupStudyId }); -async function getCheckoutData(): Promise<{ - study: Study; - terms: Terms[]; - methods: PaymentMethod[]; -}> { - // 실제론 db / internal api에서 가져오기 return { - study: { - id: 'study_1', - title: '1일1코테류프를 인증 챌린지', - desc: '1인 개발자, 이제는 대세가 되었다죠.', - price: 35000, - thumbnailUrl: '', - }, - terms: [ - { - id: 'terms_usage', - label: '이용약관 동의 (필수)', - required: true, - url: '/terms/usage', - }, - ], - methods: [ - { id: 'CARD', label: '신용카드 결제' }, - { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, - ], + id: String(studyDetail.basicInfo.groupStudyId), + title: studyDetail.detailInfo.title, + desc: studyDetail.detailInfo.summary, + price: studyDetail.basicInfo.price, + thumbnailUrl: + studyDetail.detailInfo.image?.resizedImages?.[0]?.resizedImageUrl || '', }; } -export default async function CheckoutPage() { - const { study, terms, methods } = await getCheckoutData(); - - // const clientKey = 'test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm'; - // const tossPayments = await loadTossPayments(clientKey); - - // const customerKey = "pfROxUh11lHWaPrBxzIBN"; - // const widgets = tossPayments.widgets({ - // customerKey, - // }); +export default async function CheckoutPage({ params }: PaymentPageProps) { + const { id } = await params; + const study = await getStudyData(Number(id)); return (
    @@ -88,12 +56,7 @@ export default async function CheckoutPage() { {/* 클라 렌더: 약관/결제수단/결제하기 */}
    - +
    diff --git a/src/app/(service)/payment/fail/page.tsx b/src/app/(service)/payment/fail/page.tsx new file mode 100644 index 00000000..2fa5c423 --- /dev/null +++ b/src/app/(service)/payment/fail/page.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { useSearchParams, useRouter } from 'next/navigation'; +import Button from '@/components/ui/button'; + +// 토스페이먼츠 에러 코드별 사용자 친화적 메시지 +const ERROR_MESSAGES: Record = { + PAY_PROCESS_CANCELED: '결제가 취소되었습니다.', + PAY_PROCESS_ABORTED: '결제가 중단되었습니다.', + REJECT_CARD_COMPANY: '카드사에서 결제를 거절했습니다.', + INVALID_CARD_EXPIRATION: '카드 유효기간이 만료되었습니다.', + INVALID_STOPPED_CARD: '정지된 카드입니다.', + INVALID_CARD_LOST: '분실 신고된 카드입니다.', + INVALID_CARD_NUMBER: '카드 번호가 올바르지 않습니다.', + EXCEED_MAX_DAILY_PAYMENT_COUNT: '일일 결제 한도를 초과했습니다.', + EXCEED_MAX_PAYMENT_AMOUNT: '결제 금액 한도를 초과했습니다.', + NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT: + '할부가 지원되지 않는 카드입니다.', + INVALID_CARD_INSTALLMENT_PLAN: '할부 개월 수가 올바르지 않습니다.', + NOT_ALLOWED_POINT_USE: '포인트 사용이 불가한 카드입니다.', + INVALID_API_KEY: '결제 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + INVALID_ORDER_ID: '주문 정보가 올바르지 않습니다.', + INVALID_AMOUNT: '결제 금액이 올바르지 않습니다.', +}; + +function getErrorMessage(code: string | null, message: string | null): string { + if (code && ERROR_MESSAGES[code]) { + return ERROR_MESSAGES[code]; + } + + if (message) { + return message; + } + + return '결제 중 오류가 발생했습니다. 다시 시도해주세요.'; +} + +export default function PaymentFailPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const code = searchParams.get('code'); + const message = searchParams.get('message'); + const orderId = searchParams.get('orderId'); + + const errorMessage = getErrorMessage(code, message); + + return ( +
    +
    +
    + +
    +

    결제에 실패했습니다

    +

    {errorMessage}

    + + {(code || orderId) && ( +
    + {orderId && ( +
    + 주문번호 + {orderId} +
    + )} + {code && ( +
    + 에러코드 + + {code} + +
    + )} +
    + )} + +
    + + +
    +
    +
    + ); +} diff --git a/src/app/(service)/payment/success/page.tsx b/src/app/(service)/payment/success/page.tsx new file mode 100644 index 00000000..5491d15f --- /dev/null +++ b/src/app/(service)/payment/success/page.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useSearchParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import Button from '@/components/ui/button'; + +interface PaymentConfirmResponse { + success: boolean; + message?: string; + data?: { + orderId: string; + amount: number; + paymentKey: string; + }; +} + +// TODO: 백엔드 API 호출 함수 - 실제 API 엔드포인트로 교체 필요 +async function confirmPayment( + paymentKey: string, + orderId: string, + amount: number, +): Promise { + // TODO: 실제 백엔드 API로 교체 + // const response = await fetch('/api/payment/confirm', { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ paymentKey, orderId, amount }), + // }); + // return response.json(); + + // 임시 성공 응답 + console.log('결제 승인 요청:', { paymentKey, orderId, amount }); + + return { + success: true, + data: { + orderId, + amount, + paymentKey, + }, + }; +} + +export default function PaymentSuccessPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const [status, setStatus] = useState<'loading' | 'success' | 'error'>( + 'loading', + ); + const [errorMessage, setErrorMessage] = useState(null); + + const paymentKey = searchParams.get('paymentKey'); + const orderId = searchParams.get('orderId'); + const amount = searchParams.get('amount'); + + useEffect(() => { + async function confirm() { + if (!paymentKey || !orderId || !amount) { + setStatus('error'); + setErrorMessage('결제 정보가 올바르지 않습니다.'); + + return; + } + + try { + const result = await confirmPayment( + paymentKey, + orderId, + Number(amount), + ); + + if (result.success) { + setStatus('success'); + } else { + setStatus('error'); + setErrorMessage(result.message || '결제 승인에 실패했습니다.'); + } + } catch { + setStatus('error'); + setErrorMessage('결제 승인 중 오류가 발생했습니다.'); + } + } + + confirm(); + }, [paymentKey, orderId, amount]); + + if (status === 'loading') { + return ( +
    +
    +
    +

    + 결제를 처리하고 있습니다... +

    +
    +
    + ); + } + + if (status === 'error') { + return ( +
    +
    +
    + +
    +

    결제 승인 실패

    +

    {errorMessage}

    + +
    +
    + ); + } + + return ( +
    +
    +
    + +
    +

    결제가 완료되었습니다

    +

    + 스터디 참여가 정상적으로 처리되었습니다. +

    + +
    +
    + 주문번호 + {orderId} +
    +
    + 결제금액 + + {Number(amount).toLocaleString()}원 + +
    +
    + +
    + + +
    +
    +
    + ); +} diff --git a/src/app/(service)/premium-study/[id]/page.tsx b/src/app/(service)/premium-study/[id]/page.tsx new file mode 100644 index 00000000..e7c8a752 --- /dev/null +++ b/src/app/(service)/premium-study/[id]/page.tsx @@ -0,0 +1,112 @@ +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; +import type { Metadata } from 'next'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import { Configuration } from '@/api/openapi/configuration'; +import type { GroupStudyFullResponseDto } from '@/api/openapi/models'; +import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; +import { getGroupStudyMyStatusInServer } from '@/features/study/group/api/get-group-study-my-status.server'; +import { GroupStudyDetailResponse } from '@/features/study/group/api/group-study-types'; +import PremiumStudyDetailPage from '@/features/study/premium/ui/premium-study-detail-page'; +import { getServerCookie } from '@/utils/server-cookie'; + +interface Props { + params: Promise<{ id: string }>; +} + +interface GroupStudyResponse { + content?: GroupStudyFullResponseDto; +} + +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + + try { + const config = new Configuration({ + basePath: process.env.NEXT_PUBLIC_API_BASE_URL, + }); + + const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); + + const response = await groupStudyApi.getGroupStudy(Number(id)); + const groupStudy = (response.data as GroupStudyResponse)?.content; + + if (!groupStudy) { + return { + title: '멘토 스터디 - 제로원', + description: '제로원 스터디 플랫폼에서 멘토 스터디를 둘러보세요.', + }; + } + + const title = groupStudy.detailInfo?.title || '멘토 스터디'; + const description = + groupStudy.detailInfo?.description || + groupStudy.detailInfo?.summary || + '제로원 멘토 스터디에 참여하세요.'; + + return { + title: `${title} - 제로원 멘토스터디`, + description, + openGraph: { + title: `${title} - 제로원 멘토스터디`, + description, + images: groupStudy.detailInfo?.image?.resizedImages?.[0] + ?.resizedImageUrl + ? [groupStudy.detailInfo.image.resizedImages[0].resizedImageUrl] + : [], + }, + }; + } catch { + return { + title: '멘토 스터디 - 제로원', + description: '제로원 스터디 플랫폼에서 멘토 스터디를 둘러보세요.', + }; + } +} + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + const queryClient = new QueryClient(); + + // 프리미엄 스터디 상세 정보 미리 가져오기 + await queryClient.fetchQuery({ + queryKey: ['groupStudyDetail', Number(id)], + queryFn: () => getGroupStudyDetailInServer({ groupStudyId: Number(id) }), + }); + + const data: GroupStudyDetailResponse = queryClient.getQueryData([ + 'groupStudyDetail', + Number(id), + ]); + + const memberIdStr = await getServerCookie('memberId'); + const memberId = memberIdStr ? Number(memberIdStr) : undefined; + + const isLeader = data.basicInfo.leader.memberId === memberId; + + if (!isLeader && memberId) { + // 내가 리더가 아닐 경우에만 내 신청 상태 정보 미리 가져오기 + await queryClient.prefetchQuery({ + queryKey: ['groupStudyMyStatus', Number(id)], + queryFn: () => + getGroupStudyMyStatusInServer({ groupStudyId: Number(id) }), + }); + } + + return ( + + + + ); +} diff --git a/src/app/(service)/premium-study/page.tsx b/src/app/(service)/premium-study/page.tsx new file mode 100644 index 00000000..b2b8ecfc --- /dev/null +++ b/src/app/(service)/premium-study/page.tsx @@ -0,0 +1,65 @@ +import { Plus } from 'lucide-react'; +import { createApiServerInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import Button from '@/components/ui/button'; +import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; +import PremiumStudyList from '@/features/study/premium/ui/premium-study-list'; +import PremiumStudyPagination from '@/features/study/premium/ui/premium-study-pagination'; + +interface PremiumStudyPageProps { + searchParams: Promise<{ page?: string }>; +} + +export default async function PremiumStudyPage({ + searchParams, +}: PremiumStudyPageProps) { + const params = await searchParams; + const currentPage = Number(params.page) || 1; + const pageSize = 9; + + const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudies( + 'PREMIUM_STUDY', + currentPage, + pageSize, + ); + + console.log('data', data); + + return ( +
    + {/* 헤더 */} +
    +

    + 멘토스터디 둘러보기 +

    + } + iconPosition="left" + > + 스터디 개설하기 + + } + /> +
    + + {/* 스터디 카드 그리드 */} + + + {/* 페이지네이션 */} + {data.totalPages > 1 && ( + + )} +
    + ); +} diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx deleted file mode 100644 index bc3bc409..00000000 --- a/src/app/(service)/study/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; -import { Configuration } from '@/api/openapi/configuration'; -import { GroupStudyFullResponseDto } from '@/api/openapi/models'; -import Button from '@/components/ui/button'; -import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; -import GroupStudyList from '@/features/study/group/ui/group-study-list'; -import IconPlus from '@/shared/icons/plus.svg'; -import { getServerCookie } from '@/utils/server-cookie'; -import Sidebar from '@/widgets/home/sidebar'; - -interface GroupStudyResponse { - content?: GroupStudyFullResponseDto; -} - -export default async function Study() { - const memberIdStr = await getServerCookie('memberId'); - const isLoggedIn = !!memberIdStr; - - // const config = new Configuration({ - // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, - // }); - - // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); - - // const response = await groupStudyApi.getGroupStudies(); - // const groupStudy = (response.data as GroupStudyResponse)?.content; - - // console.log('groupStudy', groupStudy); - - return ( -
    -
    -
    - - 스터디 둘러보기 - - } - > - 스터디 개설하기 - - } - /> -
    - -
    - {isLoggedIn && } -
    - ); -} diff --git a/src/components/payment/PaymentTermsModal.tsx b/src/components/payment/PaymentTermsModal.tsx new file mode 100644 index 00000000..dedcf478 --- /dev/null +++ b/src/components/payment/PaymentTermsModal.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { XIcon } from 'lucide-react'; +import Button from '../ui/button'; +import { Modal } from '../ui/modal'; + +export default function PaymentTermsModal() { + return ( + + + 내용보기 + + + + + + ZeroOne IT 서비스 이용약관 + + + + + +

    서비스 이용약관

    + +
    +

    1. 결제 및 서비스 제공

    +

    + 본 스터디 프로그램은 4주 과정의 온라인 학습 서비스이며, 결제 + 완료 시 즉시 서비스 이용이 가능하도록 구성됩니다. +

    +
    + +
    +

    2. 환불 가능 기간

    +

    + 서비스 특성상 다음과 같은 환불 기준을 적용합니다. +

    + +
    +
    +

    • 전액 환불

    +
      +
    • 결제일로부터 7일 이내
    • +
    • + 스터디 자료 열람 및 참여 이력이 0회일 경우 가능(예: 강의 + 시청, 자료 다운로드, 미션 수행, 커뮤니티 글 열람/참여 등) +
    • +
    +
    + +
    +

    • 부분 환불

    +
      +
    • + 아래 조건을 모두 충족할 경우 부분 환불이 가능합니다. +
    • +
    • 결제일로부터 7일 이내
    • +
    • 스터디 자료 사용 또는 참여 이력이 있을 경우
    • +
    • + 계산 방식 : 환불액 = 결제금액 - 총 금액 × (이용일수/총 + 스터디 일수) +
    • +
    • 콘텐츠/세션 이용 횟수에 따른 차감 후 환불
    • +
    +
    + +
    +

    • 환불 불가

    +
      +
    • 결제일로부터 7일 경과 후
    • +
    • 서비스 기간의 50% 이상 이용 시
    • +
    • + 스터디 자료 다운로드, 핵심 콘텐츠 접근, 참여 등으로 인해 + 회수 불가능한 가치 제공이 완료된 경우 +
    • +
    • + 회원의 과실, 단순 변심, 개인 일정 등의 사유로 서비스 이용이 + 어려운 경우 +
    • +
    +
    +
    +
    + +
    +

    3. 환불 접수 방법

    +
      +
    1. 환불 요청은 이메일 또는 채널톡/문의 폼을 통해 접수
    2. +
    3. + 환불 시 신분 확인을 위해 결제 정보 및 환불 계좌가 필요할 수 + 있음 +
    4. +
    5. 환불 처리는 접수 후 영업일 기준 3~5일 소요될 수 있음
    6. +
    +
    + +
    +

    4. 강제 퇴장/정책 위반

    +

    + 커뮤니티 규칙 위반, 불법 공유, 타인 비방 등 운영 정책을 위반한 + 경우 별도 환불 없이 서비스 이용이 제한될 수 있습니다. +

    +
    +
    + + + + + +
    +
    +
    + ); +} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx index 77a77253..c6e86371 100644 --- a/src/components/payment/paymentActionClient.tsx +++ b/src/components/payment/paymentActionClient.tsx @@ -1,15 +1,22 @@ 'use client'; import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; -import { useEffect, useMemo, useState } from 'react'; -import { cn } from '../ui/(shadcn)/lib/utils'; +import { useEffect, useState } from 'react'; import Button from '../ui/button'; import Checkbox from '../ui/checkbox'; import { RadioGroup, RadioGroupItem } from '../ui/radio'; +import PaymentTermsModal from './PaymentTermsModal'; + +interface Study { + id: string; + title: string; + desc: string; + price: number; + thumbnailUrl?: string; +} interface Props { - orderId: string; - amount: number; + study: Study; } type PaymentMethod = 'CARD' | 'VBANK'; @@ -21,10 +28,7 @@ const methods: { id: PaymentMethod; label: string }[] = [ { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, ]; -export default function PaymentCheckoutPage({ - orderId, - // amount, -}: Props) { +export default function PaymentCheckoutPage({ study }: Props) { const [payment, setPayment] = useState(null); const [isAgreed, setIsAgreed] = useState(false); @@ -55,19 +59,21 @@ export default function PaymentCheckoutPage({ try { await payment.requestPayment({ - method: 'CARD', // 카드 및 간편결제 - amount: 50000, // 결제 금액 - orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 - orderName: '토스 티셔츠 외 2건', - successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL - failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + method: paymentMethod, + amount: { + currency: 'KRW', + value: study.price, + }, + orderId: study.id, + orderName: study.title, + successUrl: window.location.origin + '/payment/success', + failUrl: window.location.origin + '/payment/fail', customerEmail: 'customer123@gmail.com', customerName: '김토스', customerMobilePhone: '01012341234', - // 카드 결제에 필요한 정보 card: { useEscrow: false, - flowMode: 'DEFAULT', // 통합결제창 여는 옵션 + flowMode: 'DEFAULT', useCardPoint: false, useAppCardOnly: false, }, @@ -83,10 +89,6 @@ export default function PaymentCheckoutPage({ } }; - const [amount] = useState({ - currency: 'KRW', - value: 50000, - }); useEffect(() => { async function fetchPayment() { @@ -127,14 +129,7 @@ export default function PaymentCheckoutPage({
    - - 내용보기 - +
    @@ -151,18 +146,18 @@ export default function PaymentCheckoutPage({ const selected = paymentMethod === m.id; return ( - + + {m.label} + ); })} diff --git a/src/features/my-page/ui/my-study-info-card.tsx b/src/features/my-page/ui/my-study-info-card.tsx index 5f5ee735..42eb540d 100644 --- a/src/features/my-page/ui/my-study-info-card.tsx +++ b/src/features/my-page/ui/my-study-info-card.tsx @@ -27,7 +27,7 @@ export default function MyStudyInfoCard({ return (
  • - +
    => { + const { page, size, status } = params; + + const { data } = await axiosServerInstance.get('/group-studies', { + params: { + page, + 'page-size': size, + groupStudyStatus: status, + }, + }); + + if (data.statusCode !== 200) { + throw new Error('Failed to fetch group study list'); + } + + return data.content; +}; diff --git a/src/features/study/group/api/get-group-study-list.ts b/src/features/study/group/api/get-group-study-list.ts index 4efe79c2..9fef5779 100644 --- a/src/features/study/group/api/get-group-study-list.ts +++ b/src/features/study/group/api/get-group-study-list.ts @@ -8,7 +8,7 @@ import { export const getGroupStudyList = async ( params: GroupStudyListRequest, ): Promise => { - const { page, size, status } = params; + const { page, size, status, classification } = params; try { const { data } = await axiosInstance.get('/group-studies', { @@ -16,6 +16,7 @@ export const getGroupStudyList = async ( page, 'page-size': size, groupStudyStatus: status, + classification: classification, }, }); diff --git a/src/features/study/group/model/group-study-form.schema.ts b/src/features/study/group/model/group-study-form.schema.ts index c05b650f..e3812a95 100644 --- a/src/features/study/group/model/group-study-form.schema.ts +++ b/src/features/study/group/model/group-study-form.schema.ts @@ -11,7 +11,11 @@ import { const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; +export const STUDY_CLASSIFICATION = ['GROUP_STUDY', 'PREMIUM_STUDY'] as const; +export type StudyClassification = (typeof STUDY_CLASSIFICATION)[number]; + export const GroupStudyFormSchema = z.object({ + classification: z.enum(STUDY_CLASSIFICATION), type: z.enum(STUDY_TYPES), targetRoles: z .array(z.enum(TARGET_ROLE_OPTIONS)) @@ -60,8 +64,11 @@ export type GroupStudyFormValues = z.input & { }; export type OpenGroupParsedValues = z.output; -export function buildOpenGroupDefaultValues(): GroupStudyFormValues { +export function buildOpenGroupDefaultValues( + classification: StudyClassification = 'GROUP_STUDY', +): GroupStudyFormValues { return { + classification, type: 'PROJECT', targetRoles: [], maxMembersCount: '', @@ -80,9 +87,9 @@ export function buildOpenGroupDefaultValues(): GroupStudyFormValues { }; } -export function toOpenGroupRequest( - v: OpenGroupParsedValues, -): GroupStudyFormRequest { +export function toOpenGroupRequest(v: OpenGroupParsedValues): GroupStudyFormRequest { + const isPremiumStudy = v.classification === 'PREMIUM_STUDY'; + return { basicInfo: { type: v.type, @@ -94,7 +101,7 @@ export function toOpenGroupRequest( location: v.location.trim(), startDate: v.startDate.trim(), endDate: v.endDate.trim(), - price: Number(v.price), + price: isPremiumStudy ? Number(v.price) || 0 : 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, diff --git a/src/features/study/group/model/use-group-study-list-query.ts b/src/features/study/group/model/use-group-study-list-query.ts index 4b6b3c60..8a236c45 100644 --- a/src/features/study/group/model/use-group-study-list-query.ts +++ b/src/features/study/group/model/use-group-study-list-query.ts @@ -9,6 +9,7 @@ export const useGroupStudyListQuery = () => { page: pageParam, size: 20, status: 'RECRUITING', + classification: 'GROUP_STUDY', }); return response; diff --git a/src/features/study/group/ui/group-study-form-modal.tsx b/src/features/study/group/ui/group-study-form-modal.tsx index 4f6ef0ef..47748deb 100644 --- a/src/features/study/group/ui/group-study-form-modal.tsx +++ b/src/features/study/group/ui/group-study-form-modal.tsx @@ -19,16 +19,20 @@ import { import { buildOpenGroupDefaultValues, GroupStudyFormValues, + StudyClassification, toOpenGroupRequest, } from '../model/group-study-form.schema'; import { useGroupStudyDetailQuery } from '../model/use-study-query'; +export type { StudyClassification }; + interface GroupStudyModalProps { trigger?: React.ReactNode; open?: boolean; onOpenChange?: () => void; mode: 'create' | 'edit'; groupStudyId?: number; + classification?: StudyClassification; } export default function GroupStudyFormModal({ @@ -37,6 +41,7 @@ export default function GroupStudyFormModal({ open: controlledOpen = false, groupStudyId, onOpenChange: onControlledOpen, + classification = 'GROUP_STUDY', }: GroupStudyModalProps) { const qc = useQueryClient(); const [open, setOpen] = useState(false); @@ -81,6 +86,7 @@ export default function GroupStudyFormModal({ if (isLoading) return; return { + classification, type: value.basicInfo.type, targetRoles: value.basicInfo.targetRoles, maxMembersCount: value.basicInfo.maxMembersCount.toString(), @@ -213,7 +219,7 @@ export default function GroupStudyFormModal({ ('GROUP_STUDY'); +export const useClassification = () => useContext(ClassificationContext); + interface GroupStudyFormProps { defaultValues: GroupStudyFormValues; onSubmit: (values: GroupStudyFormValues) => void; @@ -27,6 +31,7 @@ const STEP_FIELDS: Record<1 | 2 | 3, (keyof GroupStudyFormValues)[]> = { 'regularMeeting', 'startDate', 'endDate', + 'price', ], 2: ['thumbnailExtension', 'title', 'description', 'summary'], 3: ['interviewPost'], @@ -42,7 +47,8 @@ export default function GroupStudyForm({ defaultValues: defaultValues, }); - const { handleSubmit, trigger, formState } = methods; + const { handleSubmit, trigger, formState, watch } = methods; + const classification = watch('classification'); const [step, setStep] = useState<1 | 2 | 3>(1); @@ -63,7 +69,7 @@ export default function GroupStudyForm({ }; return ( - <> + @@ -124,7 +130,7 @@ export default function GroupStudyForm({ )}
    - + ); } diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 6f355e81..431f3729 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -1,199 +1,159 @@ 'use client'; import { sendGTMEvent } from '@next/third-parties/google'; -import { Loader2 } from 'lucide-react'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; +import Link from 'next/link'; import Badge from '@/components/ui/badge'; -import { useIntersectionObserver } from '@/hooks/common/use-intersection-observer'; +import Button from '@/components/ui/button'; import { useAuth } from '@/hooks/use-auth'; import { hashValue } from '@/utils/hash'; -import { BasicInfoDetail } from '../api/group-study-types'; -import { - EXPERIENCE_LEVEL_LABELS, - REGULAR_MEETING_LABELS, - ROLE_LABELS, - STUDY_TYPE_LABELS, -} from '../const/group-study-const'; -import { useGroupStudyListQuery } from '../model/use-group-study-list-query'; +import { GroupStudyData } from '../api/group-study-types'; +import { REGULAR_MEETING_LABELS } from '../const/group-study-const'; interface GroupStudyListProps { - isLoggedIn: boolean; + studies: GroupStudyData[]; } -export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { - const router = useRouter(); +export default function GroupStudyList({ studies }: GroupStudyListProps) { const { data: authData } = useAuth(); - const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = - useGroupStudyListQuery(); - const groupStudyList = data?.pages.flatMap((page) => page.content) || []; - - // useIntersectionObserver 커스텀 훅 사용 - const sentinelRef = useIntersectionObserver( - async () => { - if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage(); - } - }, - { - rootMargin: '200px 0px', - enabled: !!hasNextPage, - }, - ); - - const basicInfoItems = (basicInfo: BasicInfoDetail) => { - const { - type, - targetRoles, - experienceLevels, - regularMeeting, - maxMembersCount, - price, - approvedCount, - } = basicInfo; - - // 타입 변환 - const typeLabel = STUDY_TYPE_LABELS[type]; - - // 역할 변환 - const targetRolesLabel = targetRoles - .map((role) => { - return ROLE_LABELS[role]; - }) - .join(', '); - - // 경력 변환 - const experienceLabel = - experienceLevels - .map((level) => { - return EXPERIENCE_LEVEL_LABELS[level]; - }) - .join(', ') || '무관'; - - // 정기모임 - const frequencyLabel = REGULAR_MEETING_LABELS[regularMeeting]; - - // 참가비 - const priceLabel = price === 0 ? '무료' : `${price.toLocaleString()}원`; - - return [ - { label: '유형', value: typeLabel }, - { label: '주제', value: targetRolesLabel }, - { label: '경력', value: experienceLabel }, - { label: '정기모임', value: frequencyLabel }, - { label: '모집인원', value: `${approvedCount}/${maxMembersCount}` }, - { label: '참가비', value: priceLabel }, - ]; + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'group_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); }; - if (isLoading) { + if (studies.length === 0) { return ( -
    - +
    + 현재 그룹 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    ); } return ( - <> - {groupStudyList.length > 0 ? ( - <> -
    - {groupStudyList.map((study, index) => { - return ( -
    { - sendGTMEvent({ - event: 'group_study_detail_view', - dl_timestamp: new Date().toISOString(), - ...(authData?.memberId && { - dl_member_id: hashValue(String(authData.memberId)), - }), - dl_study_id: String(study.basicInfo.groupStudyId), - dl_study_title: study.simpleDetailInfo.title, - }); - router.push(`study/${study.basicInfo.groupStudyId}`); - }} - > -
    -
    -
    - {study.basicInfo.hostType === 'ZEROONE' && ( - 제로원 스터디 - )} - - - {study.simpleDetailInfo.title} - -
    -

    - {study.simpleDetailInfo.summary} -

    -
    -
    - {basicInfoItems(study.basicInfo).map((item, idx) => ( -
    - - {item.label} - - - {item.value} - -
    - ))} -
    -
    - {/* thumbnail */} -
    - ); - })} -
    - {/* 아래는 다음 페이지 데이터를 불러오기 위한 요소입니다. */} -
    } - className="h-10 w-full" - > - {isFetchingNextPage && ( -
    - +
    + {studies.map((study) => ( + handleStudyClick(study)} + className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" + > + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} + {study.basicInfo.hostType === 'ZEROONE' && ( +
    + 제로원 스터디
    )}
    - - ) : ( -
    - 현재 그룹 스터디가 없습니다. -
    - - 스터디가 아직 준비되지 않았습니다. - - - 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. - + {/* 컨텐츠 영역 */} +
    + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 리더 정보 */} +
    +
    +
    + {/* {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} */} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    +
    + + {study.basicInfo.approvedCount}/ + {study.basicInfo.maxMembersCount}명 + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 가격 및 버튼 */} +
    + + {study.basicInfo.price === 0 + ? '무료' + : `${study.basicInfo.price.toLocaleString()}원`} + + +
    -
    - )} - + + ))} +
    ); } diff --git a/src/features/study/group/ui/group-study-pagination.tsx b/src/features/study/group/ui/group-study-pagination.tsx new file mode 100644 index 00000000..9219a6c6 --- /dev/null +++ b/src/features/study/group/ui/group-study-pagination.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import Pagination from '@/components/ui/pagination'; + +interface GroupStudyPaginationProps { + currentPage: number; + totalPages: number; +} + +export default function GroupStudyPagination({ + currentPage, + totalPages, +}: GroupStudyPaginationProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleChangePage = (page: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', String(page)); + router.push(`/group-study?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index 8c7508aa..600126ba 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -23,6 +23,7 @@ import { REGULAR_MEETING_LABELS, } from '../../const/group-study-const'; import { GroupStudyFormValues } from '../../model/group-study-form.schema'; +import { useClassification } from '../group-study-form'; const methodOptions = STUDY_METHODS.map((v) => ({ label: STUDY_METHOD_LABELS[v], @@ -37,6 +38,9 @@ const memberOptions = Array.from({ length: 20 }, (_, i) => { export default function Step1OpenGroupStudy() { const { control, formState, watch } = useFormContext(); + const classification = useClassification(); + const isPremiumStudy = classification === 'PREMIUM_STUDY'; + const { field: typeField } = useController({ name: 'type', control, @@ -233,17 +237,29 @@ export default function Step1OpenGroupStudy() { )}
    - {/* API에는 있는데 디자인에는 없음. 뭐지??? */} - {/* - name="price" - label="참가비" - helper="참가비가 있다면 입력해주세요. (0원 가능)" - direction="vertical" - size="medium" - required - > - - */} + {isPremiumStudy && ( + + name="price" + label="참가비" + helper="참가비가 있다면 입력해주세요. (0원 가능)" + direction="vertical" + size="medium" + > + ( + + )} + /> + + )} ); } diff --git a/src/features/study/premium/ui/premium-study-detail-page.tsx b/src/features/study/premium/ui/premium-study-detail-page.tsx new file mode 100644 index 00000000..81a7c42e --- /dev/null +++ b/src/features/study/premium/ui/premium-study-detail-page.tsx @@ -0,0 +1,238 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import MoreMenu from '@/components/ui/dropdown/more-menu'; +import Tabs from '@/components/ui/tabs'; +import { useLeaderStore } from '@/stores/useLeaderStore'; +import ConfirmDeleteModal from '../../group/ui/confirm-delete-modal'; +import GroupStudyFormModal from '../../group/ui/group-study-form-modal'; +import GroupStudyMemberList from '../../group/ui/group-study-member-list'; +import PremiumStudyInfoSection from './premium-study-info-section'; +import ChannelSection from '../../group/channel/ui/channel-section'; +import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; +import { + useCompleteGroupStudyMutation, + useDeleteGroupStudyMutation, + useGroupStudyDetailQuery, +} from '../../group/model/use-study-query'; + +type ActiveTab = 'intro' | 'members' | 'channel'; + +type ActionKey = 'end' | 'delete'; + +interface PremiumStudyDetailPageProps { + groupStudyId: number; + memberId?: number; +} + +const TABS = [ + { label: '스터디 소개', value: 'intro' }, + { label: '참가자', value: 'members' }, + { label: '채널', value: 'channel' }, +]; + +export default function PremiumStudyDetailPage({ + groupStudyId, + memberId, +}: PremiumStudyDetailPageProps) { + const router = useRouter(); + + const { data: studyDetail, isLoading } = + useGroupStudyDetailQuery(groupStudyId); + + const leaderId = studyDetail?.basicInfo.leader.memberId; + + const isLeader = leaderId === memberId; + + const [active, setActive] = useState('intro'); + const [showModal, setShowModal] = useState(false); + const [action, setAction] = useState(null); + const [showStudyFormModal, setShowStudyFormModal] = useState(false); + + const setLeaderInfo = useLeaderStore((s) => s.setLeaderInfo); + + const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ + groupStudyId, + isLeader, + }); + + useEffect(() => { + const leader = studyDetail.basicInfo.leader; + setLeaderInfo(leader); + }, [studyDetail, setLeaderInfo]); + + const { mutate: deleteGroupStudy } = useDeleteGroupStudyMutation(); + const { mutate: completeStudy } = useCompleteGroupStudyMutation(); + + const ModalContent = { + end: { + title: '스터디를 종료하시겠어요?', + content: ( + <> + 종료 후에는 더 이상 모집/활동이 불가합니다. +
    이 동작은 되돌릴 수 없습니다. + + ), + confirmText: '스터디 종료', + onConfirm: () => { + completeStudy( + { groupStudyId }, + { + onSuccess: () => { + sendGTMEvent({ + event: 'premium_study_end', + group_study_id: String(groupStudyId), + }); + alert('스터디가 종료되었습니다.'); + }, + onSettled: () => { + setShowModal(false); + router.push('/premium-study'); + }, + }, + ); + }, + }, + delete: { + title: '스터디를 삭제하시겠어요?', + content: ( + <> + 삭제 시 모든 데이터가 영구적으로 제거됩니다. +
    이 동작은 되돌릴 수 없습니다. + + ), + confirmText: '스터디 삭제', + onConfirm: () => { + deleteGroupStudy( + { groupStudyId }, + { + onSuccess: () => { + sendGTMEvent({ + event: 'premium_study_delete', + group_study_id: String(groupStudyId), + }); + alert('스터디가 삭제되었습니다.'); + }, + onError: () => { + alert('스터디 삭제에 실패하였습니다.'); + }, + onSettled: () => { + router.push('/premium-study'); + setShowModal(false); + }, + }, + ); + }, + }, + }; + + // 참가자, 채널 탭 접근 가능 여부 = 스터디 참가자 또는 방장만 가능 + const isMember = + myApplicationStatus?.status === 'APPROVED' || + myApplicationStatus?.status === 'KICKED'; + + if (isLoading || !studyDetail) { + return
    로딩중...
    ; + } + + return ( +
    + setShowModal(!showModal)} + title={ModalContent[action]?.title} + content={ModalContent[action]?.content} + confirmText={ModalContent[action]?.confirmText} + onConfirm={ModalContent[action]?.onConfirm} + /> + setShowStudyFormModal(!showStudyFormModal)} + /> + +
    +
    +

    + {studyDetail?.detailInfo.title} +

    +

    + {studyDetail?.detailInfo.summary} +

    +
    + {memberId === studyDetail.basicInfo.leader.memberId && ( + { + setShowStudyFormModal(true); + }, + }, + { + label: '스터디 종료', + value: 'end', + onMenuClick: () => { + setAction('end'); + setShowModal(true); + }, + }, + { + label: '스터디 삭제', + value: 'delete', + onMenuClick: () => { + setAction('delete'); + setShowModal(true); + }, + }, + ]} + iconSize={35} + /> + )} +
    + + {/** 탭리스트 */} + tab.value === 'intro' || isLeader || isMember, + )} + activeTab={active} + onChange={(value: ActiveTab) => { + setActive(value); + sendGTMEvent({ + event: 'premium_study_tab_change', + group_study_id: String(groupStudyId), + tab: value, + }); + }} + /> + {active === 'intro' && ( + + )} + {active === 'members' && ( + + )} + {active === 'channel' && ( + + )} +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-info-section.tsx b/src/features/study/premium/ui/premium-study-info-section.tsx new file mode 100644 index 00000000..6066407a --- /dev/null +++ b/src/features/study/premium/ui/premium-study-info-section.tsx @@ -0,0 +1,326 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import dayjs from 'dayjs'; +import { + Calendar, + Clock, + File, + Folder, + Globe, + HandCoins, + MapPin, + SignpostBig, + UserCheck, + Users, +} from 'lucide-react'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import React from 'react'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import UserAvatar from '@/components/ui/avatar'; +import Button from '@/components/ui/button'; +import { getSincerityPresetByLevelName } from '@/config/sincerity-temp-presets'; +import UserProfileModal from '@/entities/user/ui/user-profile-modal'; +import { useAuth } from '@/hooks/use-auth'; +import { hashValue } from '@/utils/hash'; +import InfoCard from '@/widgets/study/group/ui/group-detail/info-card'; +import PremiumSummaryStudyInfo from './premium-summary-study-info'; + +import { + BasicInfoDetail, + GroupStudyDetailResponse, +} from '../../group/api/group-study-types'; + +import { useApplicantsByStatusQuery } from '../../group/application/model/use-applicant-qeury'; +import { + EXPERIENCE_LEVEL_LABELS, + REGULAR_MEETING_LABELS, + ROLE_LABELS, + STUDY_METHOD_LABELS, + STUDY_STATUS_LABELS, + STUDY_TYPE_LABELS, +} from '../../group/const/group-study-const'; + +interface PremiumStudyInfoSectionProps { + study: GroupStudyDetailResponse; + groupStudyId: number; + isLeader: boolean; + memberId?: number; +} + +export default function PremiumStudyInfoSection({ + study: studyDetail, + groupStudyId, + isLeader, + memberId, +}: PremiumStudyInfoSectionProps) { + const router = useRouter(); + const { data: authData } = useAuth(); + + const { data: approvedApplicants } = useApplicantsByStatusQuery({ + groupStudyId, + status: 'APPROVED', + }); + const { data: pendingApplicants } = useApplicantsByStatusQuery({ + groupStudyId, + status: 'PENDING', + }); + + const applicants = [ + ...(approvedApplicants?.pages.flatMap(({ content }) => content) || []), + ...(pendingApplicants?.pages.flatMap(({ content }) => content) || []), + ]; + + const basicInfoItems = (basicInfo: BasicInfoDetail) => { + const getDurationText = (startDate: string, endDate: string): string => { + const start = new Date(startDate); + const end = new Date(endDate); + + const diffTime = end.getTime() - start.getTime(); + if (diffTime < 0) return '기간이 잘못되었습니다.'; + + const diffDays = diffTime / (1000 * 60 * 60 * 24); + const diffWeeks = diffDays / 7; + const diffMonths = diffDays / 30; + + return diffMonths < 1 + ? `약 ${Math.round(diffWeeks)}주` + : `약 ${Math.round(diffMonths)}개월`; + }; + + return [ + { + label: '유형', + value: STUDY_TYPE_LABELS[basicInfo.type], + icon: , + }, + { + label: '주제', + value: basicInfo.targetRoles + .map((role) => { + return ROLE_LABELS[role]; + }) + .join(', '), + icon: , + }, + { + label: '경력', + value: + basicInfo.experienceLevels + .map((level) => { + return EXPERIENCE_LEVEL_LABELS[level]; + }) + .join(', ') || '무관', + icon: , + }, + { + label: '진행 방식', + value: `${STUDY_METHOD_LABELS[basicInfo.method]}${basicInfo.location ? `, ${basicInfo.location}` : ''}`, + icon: , + }, + { + label: '진행 기간', + value: getDurationText(basicInfo.startDate, basicInfo.endDate), + icon: , + }, + { + label: '정기모임', + value: REGULAR_MEETING_LABELS[basicInfo.regularMeeting], + icon: , + }, + { + label: '모집인원', + value: `${basicInfo.maxMembersCount}명`, + icon: , + }, + { + label: '시작일자', + value: dayjs(basicInfo.startDate).format('YYYY.MM.DD'), + icon: , + }, + { + label: '참가비', + value: + basicInfo.price === 0 + ? '무료' + : `${basicInfo.price.toLocaleString()}원`, + icon: , + }, + { + label: '상태', + value: `${STUDY_STATUS_LABELS[basicInfo.status]}`, + icon: , + }, + ]; + }; + + const summaryBasicInfoItems = (basicInfo: BasicInfoDetail) => { + return [ + { + label: '주제', + value: basicInfo.targetRoles + .map((role) => { + return ROLE_LABELS[role]; + }) + .join(', '), + icon: , + }, + { + label: '정기모임', + value: `${REGULAR_MEETING_LABELS[basicInfo.regularMeeting]}, ${basicInfo.location}`, + icon: , + }, + { + label: '경력', + value: + basicInfo.experienceLevels + .map((level) => { + return EXPERIENCE_LEVEL_LABELS[level]; + }) + .join(', ') || '무관', + icon: , + }, + { + label: '모집인원', + value: `${basicInfo.maxMembersCount}명`, + icon: , + }, + ]; + }; + + return ( +
    +
    +
    + 썸네일 +
    + +
    +
    +

    스터디 소개

    +
    + {studyDetail?.detailInfo.description} +
    +
    +
    +

    기본 정보

    +
    + {basicInfoItems(studyDetail?.basicInfo).map((item) => { + return ( + + ); + })} +
    +
    +
    +
    +
    + 실시간 신청자 목록 + {`${studyDetail.basicInfo.approvedCount + studyDetail.basicInfo.pendingCount}명`} +
    + {isLeader && ( + + )} +
    + +
    + {applicants.map((data) => { + const temperPreset = getSincerityPresetByLevelName( + data.applicantInfo.sincerityTemp.levelName as string, + ); + + return ( +
    + +
    +
    +
    + {data.applicantInfo.memberNickname !== '' + ? data.applicantInfo.memberNickname + : '익명'} +
    + + {`${data.applicantInfo.sincerityTemp.temperature}`}℃ + +
    +
    + { + sendGTMEvent({ + event: 'premium_study_member_profile_click', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue( + String(authData.memberId), + ), + }), + dl_target_member_id: String( + data.applicantInfo.memberId, + ), + dl_group_study_id: String(groupStudyId), + }); + }} + > + 프로필 +
    + } + /> +
    + ); + })} +
    +
    +
    +
    + +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-list.tsx b/src/features/study/premium/ui/premium-study-list.tsx new file mode 100644 index 00000000..5c18244a --- /dev/null +++ b/src/features/study/premium/ui/premium-study-list.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import Image from 'next/image'; +import Link from 'next/link'; +import Badge from '@/components/ui/badge'; +import Button from '@/components/ui/button'; +import { useAuth } from '@/hooks/use-auth'; +import { hashValue } from '@/utils/hash'; + +import { GroupStudyData } from '../../group/api/group-study-types'; +import { REGULAR_MEETING_LABELS } from '../../group/const/group-study-const'; + +interface PremiumStudyListProps { + studies: GroupStudyData[]; +} + +export default function PremiumStudyList({ studies }: PremiumStudyListProps) { + const { data: authData } = useAuth(); + + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'premium_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); + }; + + if (studies.length === 0) { + return ( +
    + 현재 멘토 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    +
    + ); + } + + return ( +
    + {studies.map((study) => ( + handleStudyClick(study)} + className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" + > + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} + {study.basicInfo.hostType === 'ZEROONE' && ( +
    + 제로원 스터디 +
    + )} +
    + + {/* 컨텐츠 영역 */} +
    + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 리더 정보 */} +
    +
    +
    + {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    +
    + + {study.basicInfo.approvedCount}/ + {study.basicInfo.maxMembersCount}명 + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 가격 및 버튼 */} +
    + + {study.basicInfo.price === 0 + ? '무료' + : `${study.basicInfo.price.toLocaleString()}원`} + + +
    +
    + + ))} +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-pagination.tsx b/src/features/study/premium/ui/premium-study-pagination.tsx new file mode 100644 index 00000000..51851adb --- /dev/null +++ b/src/features/study/premium/ui/premium-study-pagination.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import Pagination from '@/components/ui/pagination'; + +interface PremiumStudyPaginationProps { + currentPage: number; + totalPages: number; +} + +export default function PremiumStudyPagination({ + currentPage, + totalPages, +}: PremiumStudyPaginationProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleChangePage = (page: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', String(page)); + router.push(`/premium-study?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/src/features/study/premium/ui/premium-summary-study-info.tsx b/src/features/study/premium/ui/premium-summary-study-info.tsx new file mode 100644 index 00000000..3fa717bc --- /dev/null +++ b/src/features/study/premium/ui/premium-summary-study-info.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import React from 'react'; +import Button from '@/components/ui/button'; +import { + GroupStudyDetailResponse, + GroupStudyStatus, +} from '../../group/api/group-study-types'; +import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; + +interface PremiumSummaryStudyInfoProps { + data: { + label: string; + value: string; + icon: React.ReactNode; + }[]; + title: string; + groupStudyId: number; + questions: GroupStudyDetailResponse['interviewPost']['interviewPost']; + isLeader: boolean; + groupStudyStatus: GroupStudyStatus; + approvedCount: GroupStudyDetailResponse['basicInfo']['approvedCount']; + maxMembersCount: GroupStudyDetailResponse['basicInfo']['maxMembersCount']; + price: number; + memberId?: number; +} + +export default function PremiumSummaryStudyInfo({ + data, + title, + groupStudyId, + isLeader, + groupStudyStatus, + approvedCount, + maxMembersCount, + price, + memberId, +}: PremiumSummaryStudyInfoProps) { + const router = useRouter(); + const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ + groupStudyId, + isLeader, + }); + + const isLoggedIn = typeof memberId === 'number'; + + const handleCopyURL = async () => { + await navigator.clipboard.writeText(window.location.href); + alert('스터디 링크가 복사되었습니다!'); + }; + + const handleApplyClick = () => { + // 결제 페이지로 이동 (groupStudyId를 전달) + router.push(`/payment/${groupStudyId}`); + }; + + const isApplyDisabled = + myApplicationStatus?.status !== 'NONE' || + groupStudyStatus === 'IN_PROGRESS' || + approvedCount >= maxMembersCount; + + const getButtonText = () => { + if ( + myApplicationStatus?.status === 'APPROVED' || + groupStudyStatus === 'IN_PROGRESS' + ) { + return '참여 중인 스터디'; + } + if (myApplicationStatus?.status === 'PENDING') { + return '승인 대기중'; + } + return '신청하기'; + }; + + return ( +
    +

    {title}

    +
    +
    + {data.map((item) => ( +
    +
    {item.icon}
    + + {item.value} + +
    + ))} +
    + + {/* 가격 표시 */} +
    + 참가비 + + {price === 0 ? '무료' : `${price.toLocaleString()}원`} + +
    + +
    + {!isLeader && isLoggedIn && ( + + )} + {!isLoggedIn && ( + + )} + + +
    +
    + ); +} diff --git a/src/widgets/home/header.tsx b/src/widgets/home/header.tsx index 77d5c42b..3bdfa740 100644 --- a/src/widgets/home/header.tsx +++ b/src/widgets/home/header.tsx @@ -45,8 +45,8 @@ export default async function Header() { {/* 1차 MVP에선 사용하지 않아 제외 */} From 719ab7a86b383d239b2167cb2a08677ee8fd78b8 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 20 Dec 2025 23:17:18 +0900 Subject: [PATCH 007/211] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # src/app/(service)/payment/[id]/page.tsx # src/app/(service)/study/page.tsx # src/components/payment/paymentActionClient.tsx # src/features/study/group/ui/group-study-list.tsx --- src/app/(service)/study/page.tsx | 56 ++++ .../payment/paymentActionClient.tsx | 71 +++-- .../study/group/ui/group-study-list.tsx | 300 ++++++++++-------- 3 files changed, 264 insertions(+), 163 deletions(-) create mode 100644 src/app/(service)/study/page.tsx diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx new file mode 100644 index 00000000..bc3bc409 --- /dev/null +++ b/src/app/(service)/study/page.tsx @@ -0,0 +1,56 @@ +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import { Configuration } from '@/api/openapi/configuration'; +import { GroupStudyFullResponseDto } from '@/api/openapi/models'; +import Button from '@/components/ui/button'; +import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; +import GroupStudyList from '@/features/study/group/ui/group-study-list'; +import IconPlus from '@/shared/icons/plus.svg'; +import { getServerCookie } from '@/utils/server-cookie'; +import Sidebar from '@/widgets/home/sidebar'; + +interface GroupStudyResponse { + content?: GroupStudyFullResponseDto; +} + +export default async function Study() { + const memberIdStr = await getServerCookie('memberId'); + const isLoggedIn = !!memberIdStr; + + // const config = new Configuration({ + // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, + // }); + + // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); + + // const response = await groupStudyApi.getGroupStudies(); + // const groupStudy = (response.data as GroupStudyResponse)?.content; + + // console.log('groupStudy', groupStudy); + + return ( +
    +
    +
    + + 스터디 둘러보기 + + } + > + 스터디 개설하기 + + } + /> +
    + +
    + {isLoggedIn && } +
    + ); +} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx index c6e86371..77a77253 100644 --- a/src/components/payment/paymentActionClient.tsx +++ b/src/components/payment/paymentActionClient.tsx @@ -1,22 +1,15 @@ 'use client'; import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { cn } from '../ui/(shadcn)/lib/utils'; import Button from '../ui/button'; import Checkbox from '../ui/checkbox'; import { RadioGroup, RadioGroupItem } from '../ui/radio'; -import PaymentTermsModal from './PaymentTermsModal'; - -interface Study { - id: string; - title: string; - desc: string; - price: number; - thumbnailUrl?: string; -} interface Props { - study: Study; + orderId: string; + amount: number; } type PaymentMethod = 'CARD' | 'VBANK'; @@ -28,7 +21,10 @@ const methods: { id: PaymentMethod; label: string }[] = [ { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, ]; -export default function PaymentCheckoutPage({ study }: Props) { +export default function PaymentCheckoutPage({ + orderId, + // amount, +}: Props) { const [payment, setPayment] = useState(null); const [isAgreed, setIsAgreed] = useState(false); @@ -59,21 +55,19 @@ export default function PaymentCheckoutPage({ study }: Props) { try { await payment.requestPayment({ - method: paymentMethod, - amount: { - currency: 'KRW', - value: study.price, - }, - orderId: study.id, - orderName: study.title, - successUrl: window.location.origin + '/payment/success', - failUrl: window.location.origin + '/payment/fail', + method: 'CARD', // 카드 및 간편결제 + amount: 50000, // 결제 금액 + orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 + orderName: '토스 티셔츠 외 2건', + successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL + failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL customerEmail: 'customer123@gmail.com', customerName: '김토스', customerMobilePhone: '01012341234', + // 카드 결제에 필요한 정보 card: { useEscrow: false, - flowMode: 'DEFAULT', + flowMode: 'DEFAULT', // 통합결제창 여는 옵션 useCardPoint: false, useAppCardOnly: false, }, @@ -89,6 +83,10 @@ export default function PaymentCheckoutPage({ study }: Props) { } }; + const [amount] = useState({ + currency: 'KRW', + value: 50000, + }); useEffect(() => { async function fetchPayment() { @@ -129,7 +127,14 @@ export default function PaymentCheckoutPage({ study }: Props) {
    - + + 내용보기 +
    @@ -146,18 +151,18 @@ export default function PaymentCheckoutPage({ study }: Props) { const selected = paymentMethod === m.id; return ( - + + ); })} diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 431f3729..6f355e81 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -1,159 +1,199 @@ 'use client'; import { sendGTMEvent } from '@next/third-parties/google'; +import { Loader2 } from 'lucide-react'; import Image from 'next/image'; -import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import Badge from '@/components/ui/badge'; -import Button from '@/components/ui/button'; +import { useIntersectionObserver } from '@/hooks/common/use-intersection-observer'; import { useAuth } from '@/hooks/use-auth'; import { hashValue } from '@/utils/hash'; -import { GroupStudyData } from '../api/group-study-types'; -import { REGULAR_MEETING_LABELS } from '../const/group-study-const'; +import { BasicInfoDetail } from '../api/group-study-types'; +import { + EXPERIENCE_LEVEL_LABELS, + REGULAR_MEETING_LABELS, + ROLE_LABELS, + STUDY_TYPE_LABELS, +} from '../const/group-study-const'; +import { useGroupStudyListQuery } from '../model/use-group-study-list-query'; interface GroupStudyListProps { - studies: GroupStudyData[]; + isLoggedIn: boolean; } -export default function GroupStudyList({ studies }: GroupStudyListProps) { +export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { + const router = useRouter(); const { data: authData } = useAuth(); + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useGroupStudyListQuery(); - const handleStudyClick = (study: GroupStudyData) => { - sendGTMEvent({ - event: 'group_study_detail_view', - dl_timestamp: new Date().toISOString(), - ...(authData?.memberId && { - dl_member_id: hashValue(String(authData.memberId)), - }), - dl_study_id: String(study.basicInfo.groupStudyId), - dl_study_title: study.simpleDetailInfo.title, - }); + const groupStudyList = data?.pages.flatMap((page) => page.content) || []; + + // useIntersectionObserver 커스텀 훅 사용 + const sentinelRef = useIntersectionObserver( + async () => { + if (hasNextPage && !isFetchingNextPage) { + await fetchNextPage(); + } + }, + { + rootMargin: '200px 0px', + enabled: !!hasNextPage, + }, + ); + + const basicInfoItems = (basicInfo: BasicInfoDetail) => { + const { + type, + targetRoles, + experienceLevels, + regularMeeting, + maxMembersCount, + price, + approvedCount, + } = basicInfo; + + // 타입 변환 + const typeLabel = STUDY_TYPE_LABELS[type]; + + // 역할 변환 + const targetRolesLabel = targetRoles + .map((role) => { + return ROLE_LABELS[role]; + }) + .join(', '); + + // 경력 변환 + const experienceLabel = + experienceLevels + .map((level) => { + return EXPERIENCE_LEVEL_LABELS[level]; + }) + .join(', ') || '무관'; + + // 정기모임 + const frequencyLabel = REGULAR_MEETING_LABELS[regularMeeting]; + + // 참가비 + const priceLabel = price === 0 ? '무료' : `${price.toLocaleString()}원`; + + return [ + { label: '유형', value: typeLabel }, + { label: '주제', value: targetRolesLabel }, + { label: '경력', value: experienceLabel }, + { label: '정기모임', value: frequencyLabel }, + { label: '모집인원', value: `${approvedCount}/${maxMembersCount}` }, + { label: '참가비', value: priceLabel }, + ]; }; - if (studies.length === 0) { + if (isLoading) { return ( -
    - 현재 그룹 스터디가 없습니다. -
    - - 스터디가 아직 준비되지 않았습니다. - - - 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. - -
    +
    +
    ); } return ( -
    - {studies.map((study) => ( - handleStudyClick(study)} - className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" - > - {/* 썸네일 영역 */} -
    - {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] - ?.resizedImageUrl ? ( - {study.simpleDetailInfo.title} - ) : ( -
    - ZERO ONE IT -
    - )} - {study.basicInfo.hostType === 'ZEROONE' && ( -
    - 제로원 스터디 + <> + {groupStudyList.length > 0 ? ( + <> +
    + {groupStudyList.map((study, index) => { + return ( +
    { + sendGTMEvent({ + event: 'group_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); + router.push(`study/${study.basicInfo.groupStudyId}`); + }} + > +
    +
    +
    + {study.basicInfo.hostType === 'ZEROONE' && ( + 제로원 스터디 + )} + + + {study.simpleDetailInfo.title} + +
    +

    + {study.simpleDetailInfo.summary} +

    +
    +
    + {basicInfoItems(study.basicInfo).map((item, idx) => ( +
    + + {item.label} + + + {item.value} + +
    + ))} +
    +
    + {/* thumbnail */} +
    + ); + })} +
    + {/* 아래는 다음 페이지 데이터를 불러오기 위한 요소입니다. */} +
    } + className="h-10 w-full" + > + {isFetchingNextPage && ( +
    +
    )}
    + + ) : ( +
    + 현재 그룹 스터디가 없습니다. - {/* 컨텐츠 영역 */} -
    - {/* 제목 */} -

    - {study.simpleDetailInfo.title} -

    - - {/* 설명 */} -

    - {study.simpleDetailInfo.summary} -

    - - {/* 리더 정보 */} -
    -
    -
    - {/* {study.basicInfo.leader?.profileImage?.resizedImages?.[0] - ?.resizedImageUrl ? ( - 프로필 - ) : ( - 프로필 - )} */} -
    -
    -

    - {study.basicInfo.leader?.memberName || '스터디장'} -

    -

    스터디장

    -
    -
    -
    - - {study.basicInfo.approvedCount}/ - {study.basicInfo.maxMembersCount}명 - - - {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} - -
    -
    - - {/* 가격 및 버튼 */} -
    - - {study.basicInfo.price === 0 - ? '무료' - : `${study.basicInfo.price.toLocaleString()}원`} - - -
    +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. +
    - - ))} -
    +
    + )} + ); } From 3ce975ae549617806703b7ce3a1e327e9ec96dce Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sat, 27 Dec 2025 00:04:40 +0900 Subject: [PATCH 008/211] =?UTF-8?q?=EC=9C=A0=EB=A3=8C=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(service)/study/page.tsx | 56 ---- .../payment/paymentActionClient.tsx | 71 ++--- .../study/group/ui/group-study-list.tsx | 300 ++++++++---------- 3 files changed, 163 insertions(+), 264 deletions(-) delete mode 100644 src/app/(service)/study/page.tsx diff --git a/src/app/(service)/study/page.tsx b/src/app/(service)/study/page.tsx deleted file mode 100644 index bc3bc409..00000000 --- a/src/app/(service)/study/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; -import { Configuration } from '@/api/openapi/configuration'; -import { GroupStudyFullResponseDto } from '@/api/openapi/models'; -import Button from '@/components/ui/button'; -import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; -import GroupStudyList from '@/features/study/group/ui/group-study-list'; -import IconPlus from '@/shared/icons/plus.svg'; -import { getServerCookie } from '@/utils/server-cookie'; -import Sidebar from '@/widgets/home/sidebar'; - -interface GroupStudyResponse { - content?: GroupStudyFullResponseDto; -} - -export default async function Study() { - const memberIdStr = await getServerCookie('memberId'); - const isLoggedIn = !!memberIdStr; - - // const config = new Configuration({ - // basePath: process.env.NEXT_PUBLIC_API_BASE_URL, - // }); - - // const groupStudyApi = new GroupStudyManagementApi(config, config.basePath); - - // const response = await groupStudyApi.getGroupStudies(); - // const groupStudy = (response.data as GroupStudyResponse)?.content; - - // console.log('groupStudy', groupStudy); - - return ( -
    -
    -
    - - 스터디 둘러보기 - - } - > - 스터디 개설하기 - - } - /> -
    - -
    - {isLoggedIn && } -
    - ); -} diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx index 77a77253..c6e86371 100644 --- a/src/components/payment/paymentActionClient.tsx +++ b/src/components/payment/paymentActionClient.tsx @@ -1,15 +1,22 @@ 'use client'; import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; -import { useEffect, useMemo, useState } from 'react'; -import { cn } from '../ui/(shadcn)/lib/utils'; +import { useEffect, useState } from 'react'; import Button from '../ui/button'; import Checkbox from '../ui/checkbox'; import { RadioGroup, RadioGroupItem } from '../ui/radio'; +import PaymentTermsModal from './PaymentTermsModal'; + +interface Study { + id: string; + title: string; + desc: string; + price: number; + thumbnailUrl?: string; +} interface Props { - orderId: string; - amount: number; + study: Study; } type PaymentMethod = 'CARD' | 'VBANK'; @@ -21,10 +28,7 @@ const methods: { id: PaymentMethod; label: string }[] = [ { id: 'VBANK', label: '무통장 입금 (가상계좌)' }, ]; -export default function PaymentCheckoutPage({ - orderId, - // amount, -}: Props) { +export default function PaymentCheckoutPage({ study }: Props) { const [payment, setPayment] = useState(null); const [isAgreed, setIsAgreed] = useState(false); @@ -55,19 +59,21 @@ export default function PaymentCheckoutPage({ try { await payment.requestPayment({ - method: 'CARD', // 카드 및 간편결제 - amount: 50000, // 결제 금액 - orderId: 'a0bPplgMJIXnZfBh4sSI1', // 고유 주문번호 - orderName: '토스 티셔츠 외 2건', - successUrl: window.location.origin + '/success', // 결제 요청이 성공하면 리다이렉트되는 URL - failUrl: window.location.origin + '/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + method: paymentMethod, + amount: { + currency: 'KRW', + value: study.price, + }, + orderId: study.id, + orderName: study.title, + successUrl: window.location.origin + '/payment/success', + failUrl: window.location.origin + '/payment/fail', customerEmail: 'customer123@gmail.com', customerName: '김토스', customerMobilePhone: '01012341234', - // 카드 결제에 필요한 정보 card: { useEscrow: false, - flowMode: 'DEFAULT', // 통합결제창 여는 옵션 + flowMode: 'DEFAULT', useCardPoint: false, useAppCardOnly: false, }, @@ -83,10 +89,6 @@ export default function PaymentCheckoutPage({ } }; - const [amount] = useState({ - currency: 'KRW', - value: 50000, - }); useEffect(() => { async function fetchPayment() { @@ -127,14 +129,7 @@ export default function PaymentCheckoutPage({
    - - 내용보기 - +
    @@ -151,18 +146,18 @@ export default function PaymentCheckoutPage({ const selected = paymentMethod === m.id; return ( - + + {m.label} + ); })} diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 6f355e81..431f3729 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -1,199 +1,159 @@ 'use client'; import { sendGTMEvent } from '@next/third-parties/google'; -import { Loader2 } from 'lucide-react'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; +import Link from 'next/link'; import Badge from '@/components/ui/badge'; -import { useIntersectionObserver } from '@/hooks/common/use-intersection-observer'; +import Button from '@/components/ui/button'; import { useAuth } from '@/hooks/use-auth'; import { hashValue } from '@/utils/hash'; -import { BasicInfoDetail } from '../api/group-study-types'; -import { - EXPERIENCE_LEVEL_LABELS, - REGULAR_MEETING_LABELS, - ROLE_LABELS, - STUDY_TYPE_LABELS, -} from '../const/group-study-const'; -import { useGroupStudyListQuery } from '../model/use-group-study-list-query'; +import { GroupStudyData } from '../api/group-study-types'; +import { REGULAR_MEETING_LABELS } from '../const/group-study-const'; interface GroupStudyListProps { - isLoggedIn: boolean; + studies: GroupStudyData[]; } -export default function GroupStudyList({ isLoggedIn }: GroupStudyListProps) { - const router = useRouter(); +export default function GroupStudyList({ studies }: GroupStudyListProps) { const { data: authData } = useAuth(); - const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = - useGroupStudyListQuery(); - const groupStudyList = data?.pages.flatMap((page) => page.content) || []; - - // useIntersectionObserver 커스텀 훅 사용 - const sentinelRef = useIntersectionObserver( - async () => { - if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage(); - } - }, - { - rootMargin: '200px 0px', - enabled: !!hasNextPage, - }, - ); - - const basicInfoItems = (basicInfo: BasicInfoDetail) => { - const { - type, - targetRoles, - experienceLevels, - regularMeeting, - maxMembersCount, - price, - approvedCount, - } = basicInfo; - - // 타입 변환 - const typeLabel = STUDY_TYPE_LABELS[type]; - - // 역할 변환 - const targetRolesLabel = targetRoles - .map((role) => { - return ROLE_LABELS[role]; - }) - .join(', '); - - // 경력 변환 - const experienceLabel = - experienceLevels - .map((level) => { - return EXPERIENCE_LEVEL_LABELS[level]; - }) - .join(', ') || '무관'; - - // 정기모임 - const frequencyLabel = REGULAR_MEETING_LABELS[regularMeeting]; - - // 참가비 - const priceLabel = price === 0 ? '무료' : `${price.toLocaleString()}원`; - - return [ - { label: '유형', value: typeLabel }, - { label: '주제', value: targetRolesLabel }, - { label: '경력', value: experienceLabel }, - { label: '정기모임', value: frequencyLabel }, - { label: '모집인원', value: `${approvedCount}/${maxMembersCount}` }, - { label: '참가비', value: priceLabel }, - ]; + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'group_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); }; - if (isLoading) { + if (studies.length === 0) { return ( -
    - +
    + 현재 그룹 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    ); } return ( - <> - {groupStudyList.length > 0 ? ( - <> -
    - {groupStudyList.map((study, index) => { - return ( -
    { - sendGTMEvent({ - event: 'group_study_detail_view', - dl_timestamp: new Date().toISOString(), - ...(authData?.memberId && { - dl_member_id: hashValue(String(authData.memberId)), - }), - dl_study_id: String(study.basicInfo.groupStudyId), - dl_study_title: study.simpleDetailInfo.title, - }); - router.push(`study/${study.basicInfo.groupStudyId}`); - }} - > -
    -
    -
    - {study.basicInfo.hostType === 'ZEROONE' && ( - 제로원 스터디 - )} - - - {study.simpleDetailInfo.title} - -
    -

    - {study.simpleDetailInfo.summary} -

    -
    -
    - {basicInfoItems(study.basicInfo).map((item, idx) => ( -
    - - {item.label} - - - {item.value} - -
    - ))} -
    -
    - {/* thumbnail */} -
    - ); - })} -
    - {/* 아래는 다음 페이지 데이터를 불러오기 위한 요소입니다. */} -
    } - className="h-10 w-full" - > - {isFetchingNextPage && ( -
    - +
    + {studies.map((study) => ( + handleStudyClick(study)} + className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" + > + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} + {study.basicInfo.hostType === 'ZEROONE' && ( +
    + 제로원 스터디
    )}
    - - ) : ( -
    - 현재 그룹 스터디가 없습니다. -
    - - 스터디가 아직 준비되지 않았습니다. - - - 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. - + {/* 컨텐츠 영역 */} +
    + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 리더 정보 */} +
    +
    +
    + {/* {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} */} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    +
    + + {study.basicInfo.approvedCount}/ + {study.basicInfo.maxMembersCount}명 + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 가격 및 버튼 */} +
    + + {study.basicInfo.price === 0 + ? '무료' + : `${study.basicInfo.price.toLocaleString()}원`} + + +
    -
    - )} - + + ))} +
    ); } From e00734ed180759dd3227844fd6690267c81873c9 Mon Sep 17 00:00:00 2001 From: aken-you Date: Sat, 27 Dec 2025 16:28:45 +0900 Subject: [PATCH 009/211] =?UTF-8?q?chore:=20openapi=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=84=B4=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EB=84=88?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 74 ++++++++------- package.json | 2 +- scripts/api-generator.mjs | 186 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 36 deletions(-) create mode 100755 scripts/api-generator.mjs diff --git a/README.md b/README.md index fd1009d3..fbfa2e33 100644 --- a/README.md +++ b/README.md @@ -221,47 +221,51 @@ OpenAPI Generator로 생성된 클라이언트 사용법과 환경변수 설정 ### 새로운 API 추가 -1. **API 함수 작성** (`api/endpoints/`) +`src/hooks/queries` 디렉토리에 새로운 파일이 생성되면 자동으로 API 인스턴스 보일러플레이트 코드를 추가하는 코드 제너레이터를 사용해줍니다. - ```typescript - // api/endpoints/study.api.ts - import { axiosInstance } from '@/api/client/axios'; - import type { Study } from '@/models/study.model'; - - export const StudyAPI = { - getList: async (): Promise => { - const res = await axiosInstance.get('/studies'); - return res.data.content; - }, - }; - ``` +1. **새 파일 생성** -2. **Query Hook 추가** (`hooks/queries/`) +```bash +yarn generate:api bank-search-api +``` - ```typescript - // hooks/queries/use-study-queries.ts - import { useQuery } from '@tanstack/react-query'; - import { StudyAPI } from '@/api/endpoints/study.api'; - - export const useStudyQueries = { - useList: () => - useQuery({ - queryKey: ['study', 'list'], - queryFn: StudyAPI.getList, - }), - }; - ``` +이 명령어는 `src/hooks/queries/bank-search-api.ts` 파일을 생성하고 자동으로 보일러플레이트 코드를 추가합니다. -3. **컴포넌트에서 사용** +```typescript +import { createApiInstance } from '@/api/client/open-api-instance'; +import { BankSearchApi } from '@/api/openapi'; - ```tsx - import { useStudyQueries } from '@/hooks/queries/use-study-queries'; +const bankSearchApi = createApiInstance(BankSearchApi); +``` - export default function StudyList() { - const { data: studies } = useStudyQueries.useList(); - return
    {/* ... */}
    ; - } - ``` +여러 파일 동시 생성도 가능합니다. + +```bash +yarn generate:api payment-api settlement-api user-api +``` + +2. **hook 작성** + +파일을 만들었다면 hook을 작성해주세요. + +```typescript +// src/hooks/queries/new-api.ts +import { createApiInstance } from '@/api/client/open-api-instance'; +import { NewApi } from '@/api/openapi'; + +const newApi = createApiInstance(NewApi); // 자동 생성됨 + +// 여기서부터 hooks을 작성하세요 +export const useGetData = () => { + return useQuery({ + queryKey: ['data'], + queryFn: async () => { + const { data } = await newApi.getData(); + return data.content; + }, + }); +}; +``` ### 새로운 타입/스키마 추가 diff --git a/package.json b/package.json index 3e61c778..3d749562 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "chromatic": "chromatic --project-token=chpt_bc3a617b8073f9f", "api:on": "sh ./scripts/setup-backend.sh", "api:off": "docker-compose -f ../study-platform-mvp/docker-compose.yml down", - "api:logs": "docker-compose -f ../study-platform-mvp/docker-compose.yml logs -f mvp-app" + "generate:api": "node scripts/api-generator.mjs" }, "dependencies": { "@hookform/resolvers": "^5.2.1", diff --git a/scripts/api-generator.mjs b/scripts/api-generator.mjs new file mode 100755 index 00000000..e9c7f7d2 --- /dev/null +++ b/scripts/api-generator.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const HOOKS_QUERIES_DIR = path.join(__dirname, '..', 'src', 'hooks', 'queries'); +const WATCH_MODE = process.argv.includes('--watch'); +const args = process.argv.slice(2).filter((arg) => !arg.startsWith('--')); + +function toPascalCase(str) { + return str + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); +} + +function getApiClassName(filename) { + const baseName = filename.replace('.ts', ''); + + return toPascalCase(baseName); +} + +function generateApiBoilerplate(filename) { + const apiClassName = getApiClassName(filename); + + return `import { createApiInstance } from '@/api/client/open-api-instance'; +import { ${apiClassName} } from '@/api/openapi'; + +const ${apiClassName.charAt(0).toLowerCase() + apiClassName.slice(1)} = createApiInstance(${apiClassName}); +`; +} + +function shouldProcessFile(filename) { + if (!filename.endsWith('.ts')) return false; + if (filename.endsWith('.d.ts')) return false; + if (filename.endsWith('.test.ts')) return false; + if (filename.endsWith('.spec.ts')) return false; + + return true; +} + +function isFileEmpty(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf-8').trim(); + + return content.length === 0; + } catch (error) { + return false; + } +} + +function hasApiBoilerplate(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + return content.includes('createApiInstance'); + } catch (error) { + return false; + } +} + +function processFile(filePath, isNewFile = false) { + const filename = path.basename(filePath); + + if (!shouldProcessFile(filename)) { + return; + } + + if (!isNewFile && !isFileEmpty(filePath)) { + console.log(`⏭️ Skipping ${filename} (file is not empty)`); + + return; + } + + if (hasApiBoilerplate(filePath)) { + console.log(`⏭️ Skipping ${filename} (already has API boilerplate)`); + + return; + } + + const boilerplate = generateApiBoilerplate(filename); + + try { + fs.writeFileSync(filePath, boilerplate, 'utf-8'); + console.log(`✅ Generated boilerplate for ${filename}`); + } catch (error) { + console.error(`❌ Error writing to ${filename}:`, error.message); + } +} + +function createNewFile(filename) { + if (!filename.endsWith('.ts')) { + filename = `${filename}.ts`; + } + + const filePath = path.join(HOOKS_QUERIES_DIR, filename); + + if (fs.existsSync(filePath)) { + console.log(`⚠️ File already exists: ${filename}`); + console.log(` Use the existing file or delete it first.`); + + return; + } + + const boilerplate = generateApiBoilerplate(filename); + + try { + fs.writeFileSync(filePath, boilerplate, 'utf-8'); + console.log(`✅ Created ${filename} with boilerplate code`); + } catch (error) { + console.error(`❌ Error creating ${filename}:`, error.message); + } +} + +function scanExistingFiles() { + console.log('🔍 Scanning existing files in src/hooks/queries...\n'); + + try { + const files = fs.readdirSync(HOOKS_QUERIES_DIR); + + files.forEach((file) => { + const filePath = path.join(HOOKS_QUERIES_DIR, file); + const stats = fs.statSync(filePath); + + if (stats.isFile()) { + processFile(filePath); + } + }); + + console.log('\n✨ Scan complete!\n'); + } catch (error) { + console.error('❌ Error scanning directory:', error.message); + } +} + +function watchDirectory() { + console.log(`👀 Watching ${HOOKS_QUERIES_DIR} for new files...\n`); + + fs.watch(HOOKS_QUERIES_DIR, { recursive: false }, (eventType, filename) => { + if (!filename || eventType !== 'rename') return; + + const filePath = path.join(HOOKS_QUERIES_DIR, filename); + + if (!fs.existsSync(filePath)) return; + + const stats = fs.statSync(filePath); + if (!stats.isFile()) return; + + setTimeout(() => { + processFile(filePath); + }, 100); + }); + + console.log('Press Ctrl+C to stop watching.\n'); +} + +function main() { + console.log('🚀 API Code Generator\n'); + + if (!fs.existsSync(HOOKS_QUERIES_DIR)) { + console.error(`❌ Directory not found: ${HOOKS_QUERIES_DIR}`); + process.exit(1); + } + + // 파일명이 인자로 전달된 경우 + if (args.length > 0) { + args.forEach((filename) => { + createNewFile(filename); + }); + + return; + } + + // 인자가 없으면 기존 동작 수행 + scanExistingFiles(); + + if (WATCH_MODE) { + watchDirectory(); + } +} + +main(); From 823549356effb0d33709692edc9606b84f5fa6d8 Mon Sep 17 00:00:00 2001 From: yeun38 Date: Sun, 28 Dec 2025 10:16:30 +0900 Subject: [PATCH 010/211] =?UTF-8?q?=EA=B7=B8=EB=A3=B9=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20UI=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(service)/group-study/page.tsx | 6 +- .../api/get-group-study-detail.server.ts | 11 +- .../study/group/api/get-group-study-detail.ts | 5 + .../study/group/model/use-study-query.ts | 15 +- .../group/ui/group-study-detail-page.tsx | 9 +- .../study/group/ui/group-study-list.tsx | 104 +------ .../study/group/ui/group-study-pagination.tsx | 2 +- .../study/group/ui/study-info-section.tsx | 225 +++------------- .../study/group/ui/summary-study-info.tsx | 133 --------- src/features/study/ui/study-card.tsx | 148 ++++++++++ src/features/study/ui/summary-study-info.tsx | 253 ++++++++++++++++++ src/widgets/home/sidebar.tsx | 2 + 12 files changed, 484 insertions(+), 429 deletions(-) delete mode 100644 src/features/study/group/ui/summary-study-info.tsx create mode 100644 src/features/study/ui/study-card.tsx create mode 100644 src/features/study/ui/summary-study-info.tsx diff --git a/src/app/(service)/group-study/page.tsx b/src/app/(service)/group-study/page.tsx index 3198ea93..89a3d542 100644 --- a/src/app/(service)/group-study/page.tsx +++ b/src/app/(service)/group-study/page.tsx @@ -15,7 +15,7 @@ export default async function GroupStudyPage({ }: GroupStudyPageProps) { const params = await searchParams; const currentPage = Number(params.page) || 1; - const pageSize = 9; + const pageSize = 15; const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); @@ -54,10 +54,10 @@ export default async function GroupStudyPage({ {/* 페이지네이션 */} - {data.totalPages > 1 && ( + {data.content.totalPages > 1 && ( )}
    diff --git a/src/features/study/group/api/get-group-study-detail.server.ts b/src/features/study/group/api/get-group-study-detail.server.ts index d0758a66..d85b2c8f 100644 --- a/src/features/study/group/api/get-group-study-detail.server.ts +++ b/src/features/study/group/api/get-group-study-detail.server.ts @@ -2,6 +2,11 @@ import { isAxiosError } from 'axios'; import { notFound } from 'next/navigation'; import { isApiError } from '@/api/client/api-error'; import { axiosServerInstance } from '@/api/client/axios.server'; +import { + createApiInstance, + createApiServerInstance, +} from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; import { GroupStudyDetailRequest, GroupStudyDetailResponse, @@ -12,9 +17,11 @@ export const getGroupStudyDetailInServer = async ({ groupStudyId, }: GroupStudyDetailRequest): Promise => { try { - const res = await axiosServerInstance.get(`/group-studies/${groupStudyId}`); + const groupStudyApi = createApiServerInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudy(groupStudyId); - return res.data.content; + return data.content; } catch (error: any) { // 서버에서 그룹스터디를 찾을 수 없다는 에러가 오면 notFound 호출 if ( diff --git a/src/features/study/group/api/get-group-study-detail.ts b/src/features/study/group/api/get-group-study-detail.ts index e6efa5b6..d7bb3af0 100644 --- a/src/features/study/group/api/get-group-study-detail.ts +++ b/src/features/study/group/api/get-group-study-detail.ts @@ -1,4 +1,6 @@ import { axiosInstance } from '@/api/client/axios'; +import { createApiInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi'; import { GroupStudyDetailRequest, GroupStudyDetailResponse, @@ -11,6 +13,9 @@ export const getGroupStudyDetail = async ( const { groupStudyId } = params; try { + // const groupStudyApi = createApiInstance(GroupStudyManagementApi); + + // const { data } = await groupStudyApi.getGroupStudy(groupStudyId); const { data } = await axiosInstance.get(`/group-studies/${groupStudyId}`); if (data.statusCode !== 200) { diff --git a/src/features/study/group/model/use-study-query.ts b/src/features/study/group/model/use-study-query.ts index 29a6416a..f886c7ef 100644 --- a/src/features/study/group/model/use-study-query.ts +++ b/src/features/study/group/model/use-study-query.ts @@ -1,4 +1,6 @@ import { useMutation, useQuery } from '@tanstack/react-query'; +import { createApiInstance } from '@/api/client/open-api-instance'; +import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; import { deleteGroupStudy } from '../api/delete-group-study'; import { getGroupStudyDetail } from '../api/get-group-study-detail'; import { DeleteGroupStudyRequest } from '../api/group-study-types'; @@ -8,7 +10,18 @@ import { patchGroupStudyComplete } from '../api/patch-group-study-complete'; export const useGroupStudyDetailQuery = (groupStudyId: number) => { return useQuery({ queryKey: ['groupStudyDetail', groupStudyId], - queryFn: () => getGroupStudyDetail({ groupStudyId }), + queryFn: async () => { + try { + const groupStudyApi = createApiInstance(GroupStudyManagementApi); + + const { data } = await groupStudyApi.getGroupStudy(groupStudyId); + + return data.content; + } catch (err) { + console.error('Error in useGroupStudyDetailQuery:', err); + throw err; + } + }, enabled: !!groupStudyId, // id가 존재할 때만 실행 }); }; diff --git a/src/features/study/group/ui/group-study-detail-page.tsx b/src/features/study/group/ui/group-study-detail-page.tsx index be279a14..6cfb13b2 100644 --- a/src/features/study/group/ui/group-study-detail-page.tsx +++ b/src/features/study/group/ui/group-study-detail-page.tsx @@ -42,6 +42,8 @@ export default function StudyDetailPage({ const { data: studyDetail, isLoading } = useGroupStudyDetailQuery(groupStudyId); + console.log('data', studyDetail); + const leaderId = studyDetail?.basicInfo.leader.memberId; const isLeader = leaderId === memberId; @@ -211,12 +213,7 @@ export default function StudyDetailPage({ }} /> {active === 'intro' && ( - + )} {active === 'members' && ( {studies.map((study) => ( - handleStudyClick(study)} - className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" - > - {/* 썸네일 영역 */} -
    - {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] - ?.resizedImageUrl ? ( - {study.simpleDetailInfo.title} - ) : ( -
    - ZERO ONE IT -
    - )} - {study.basicInfo.hostType === 'ZEROONE' && ( -
    - 제로원 스터디 -
    - )} -
    - - {/* 컨텐츠 영역 */} -
    - {/* 제목 */} -

    - {study.simpleDetailInfo.title} -

    - - {/* 설명 */} -

    - {study.simpleDetailInfo.summary} -

    - - {/* 리더 정보 */} -
    -
    -
    - {/* {study.basicInfo.leader?.profileImage?.resizedImages?.[0] - ?.resizedImageUrl ? ( - 프로필 - ) : ( - 프로필 - )} */} -
    -
    -

    - {study.basicInfo.leader?.memberName || '스터디장'} -

    -

    스터디장

    -
    -
    -
    - - {study.basicInfo.approvedCount}/ - {study.basicInfo.maxMembersCount}명 - - - {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} - -
    -
    - - {/* 가격 및 버튼 */} -
    - - {study.basicInfo.price === 0 - ? '무료' - : `${study.basicInfo.price.toLocaleString()}원`} - - -
    -
    - + /> ))}
    ); diff --git a/src/features/study/group/ui/group-study-pagination.tsx b/src/features/study/group/ui/group-study-pagination.tsx index 9219a6c6..e519f0f9 100644 --- a/src/features/study/group/ui/group-study-pagination.tsx +++ b/src/features/study/group/ui/group-study-pagination.tsx @@ -26,7 +26,7 @@ export default function GroupStudyPagination({ page={currentPage} totalPages={totalPages} onChangePage={handleChangePage} - className="mt-600" + className="mt-400 py-200" /> ); } diff --git a/src/features/study/group/ui/study-info-section.tsx b/src/features/study/group/ui/study-info-section.tsx index d68a1338..13b51227 100644 --- a/src/features/study/group/ui/study-info-section.tsx +++ b/src/features/study/group/ui/study-info-section.tsx @@ -1,22 +1,8 @@ 'use client'; import { sendGTMEvent } from '@next/third-parties/google'; -import dayjs from 'dayjs'; -import { - Calendar, - Clock, - File, - Folder, - Globe, - HandCoins, - MapPin, - SignpostBig, - UserCheck, - Users, -} from 'lucide-react'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -import React from 'react'; +import { useParams, useRouter } from 'next/navigation'; import { cn } from '@/components/ui/(shadcn)/lib/utils'; import UserAvatar from '@/components/ui/avatar'; import Button from '@/components/ui/button'; @@ -24,171 +10,36 @@ import { getSincerityPresetByLevelName } from '@/config/sincerity-temp-presets'; import UserProfileModal from '@/entities/user/ui/user-profile-modal'; import { useAuth } from '@/hooks/use-auth'; import { hashValue } from '@/utils/hash'; -import InfoCard from '@/widgets/study/group/ui/group-detail/info-card'; -import SummaryStudyInfo from './summary-study-info'; +import SummaryStudyInfo from '../../ui/summary-study-info'; -import { - BasicInfoDetail, - GroupStudyDetailResponse, -} from '../api/group-study-types'; +import { GroupStudyDetailResponse } from '../api/group-study-types'; import { useApplicantsByStatusQuery } from '../application/model/use-applicant-qeury'; -import { - EXPERIENCE_LEVEL_LABELS, - REGULAR_MEETING_LABELS, - ROLE_LABELS, - STUDY_METHOD_LABELS, - STUDY_STATUS_LABELS, - STUDY_TYPE_LABELS, -} from '../const/group-study-const'; interface StudyInfoSectionProps { study: GroupStudyDetailResponse; - groupStudyId: number; isLeader: boolean; - memberId?: number; } export default function StudyInfoSection({ study: studyDetail, - groupStudyId, isLeader, - memberId, }: StudyInfoSectionProps) { const router = useRouter(); + const params = useParams(); const { data: authData } = useAuth(); + const groupStudyId = Number(params.id); + const { data: approvedApplicants } = useApplicantsByStatusQuery({ groupStudyId, status: 'APPROVED', }); - const { data: pendingApplicants } = useApplicantsByStatusQuery({ - groupStudyId, - status: 'PENDING', - }); const applicants = [ ...(approvedApplicants?.pages.flatMap(({ content }) => content) || []), - ...(pendingApplicants?.pages.flatMap(({ content }) => content) || []), ]; - const basicInfoItems = (basicInfo: BasicInfoDetail) => { - const getDurationText = (startDate: string, endDate: string): string => { - const start = new Date(startDate); - const end = new Date(endDate); - - const diffTime = end.getTime() - start.getTime(); - if (diffTime < 0) return '기간이 잘못되었습니다.'; - - const diffDays = diffTime / (1000 * 60 * 60 * 24); - const diffWeeks = diffDays / 7; - const diffMonths = diffDays / 30; // 대략적인 월 계산 (평균 30일) - - return diffMonths < 1 - ? `약 ${Math.round(diffWeeks)}주` - : `약 ${Math.round(diffMonths)}개월`; - }; - - return [ - { - label: '유형', - value: STUDY_TYPE_LABELS[basicInfo.type], - icon: , - }, - { - label: '주제', - value: basicInfo.targetRoles - .map((role) => { - return ROLE_LABELS[role]; - }) - .join(', '), - icon: , - }, - { - label: '경력', - value: - basicInfo.experienceLevels - .map((level) => { - return EXPERIENCE_LEVEL_LABELS[level]; - }) - .join(', ') || '무관', - icon: , - }, - { - label: '진행 방식', - value: `${STUDY_METHOD_LABELS[basicInfo.method]}${basicInfo.location ? `, ${basicInfo.location}` : ''}`, - icon: , - }, - { - label: '진행 기간', - value: getDurationText(basicInfo.startDate, basicInfo.endDate), - icon: , - }, - { - label: '정기모임', - value: REGULAR_MEETING_LABELS[basicInfo.regularMeeting], - icon: , - }, - { - label: '모집인원', - value: `${basicInfo.maxMembersCount}명`, - icon: , - }, - { - label: '시작일자', - value: dayjs(basicInfo.startDate).format('YYYY.MM.DD'), - icon: , - }, - { - label: '참가비', - value: - basicInfo.price === 0 - ? '무료' - : `${basicInfo.price.toLocaleString()}원`, - icon: , - }, - { - label: '상태', - value: `${STUDY_STATUS_LABELS[basicInfo.status]}`, - icon: , - }, - ]; - }; - - const summaryBasicInfoItems = (basicInfo: BasicInfoDetail) => { - return [ - { - label: '주제', - value: basicInfo.targetRoles - .map((role) => { - return ROLE_LABELS[role]; - }) - .join(', '), - icon: , - }, - { - label: '정기모임', - value: `${REGULAR_MEETING_LABELS[basicInfo.regularMeeting]}, ${basicInfo.location}`, - icon: , - }, - { - label: '경력', - value: - basicInfo.experienceLevels - .map((level) => { - return EXPERIENCE_LEVEL_LABELS[level]; - }) - .join(', ') || '무관', - icon: , - }, - { - label: '모집인원', - value: `${basicInfo.maxMembersCount}명`, - icon: , - }, - ]; - }; - return ( // todo: 스터디 공지 모달 추가 // @@ -206,31 +57,49 @@ export default function StudyInfoSection({

    스터디 소개

    +
    +
    + +
    +
    + + {studyDetail.basicInfo.leader.memberNickname} + +
    + 스터디 리더 + + + {studyDetail.basicInfo.leader.simpleIntroduction} + +
    +
    +
    +
    + + 프로필 +
    + } + /> +
    {studyDetail?.detailInfo.description}
    -
    -

    기본 정보

    - {/*
    프로필박스
    */} -
    - {basicInfoItems(studyDetail?.basicInfo).map((item) => { - return ( - - ); - })} -
    -
    +
    실시간 신청자 목록 - {`${studyDetail.basicInfo.approvedCount + studyDetail.basicInfo.pendingCount}명`} + {`${approvedApplicants?.pages.length}명`}
    {isLeader && (
    - +
    ); } diff --git a/src/features/study/group/ui/summary-study-info.tsx b/src/features/study/group/ui/summary-study-info.tsx deleted file mode 100644 index b6fdebb7..00000000 --- a/src/features/study/group/ui/summary-study-info.tsx +++ /dev/null @@ -1,133 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import React from 'react'; -import Button from '@/components/ui/button'; -import ApplyGroupStudyModal from './apply-group-study-modal'; -import { - GroupStudyDetailResponse, - GroupStudyStatus, -} from '../api/group-study-types'; -import { useGroupStudyMyStatusQuery } from '../model/use-group-study-my-status-query'; - -interface SummaryStudyInfoProps { - data: { - label: string; - value: string; - icon: React.ReactNode; - }[]; - title: string; - groupStudyId: number; - questions: GroupStudyDetailResponse['interviewPost']['interviewPost']; - isLeader: boolean; - groupStudyStatus: GroupStudyStatus; - approvedCount: GroupStudyDetailResponse['basicInfo']['approvedCount']; - maxMembersCount: GroupStudyDetailResponse['basicInfo']['maxMembersCount']; - memberId?: number; -} - -export default function SummaryStudyInfo({ - data, - title, - groupStudyId, - questions, - isLeader, - groupStudyStatus, - approvedCount, - maxMembersCount, - memberId, -}: SummaryStudyInfoProps) { - const router = useRouter(); - const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ - groupStudyId, - isLeader, - }); - - const isLoggedIn = typeof memberId === 'number'; - - const handleCopyURL = async () => { - await navigator.clipboard.writeText(window.location.href); - alert('스터디 링크가 복사되었습니다!'); - }; - - return ( -
    -

    {title}

    -
    -
    - {data.map((item) => ( -
    -
    {item.icon}
    - - {item.value} - -
    - ))} -
    - -
    - {/* 신청 이전 => "신청하기" able */} - {/* 신청 이후 => "신청하기" disabled */} - {/* 승인 이후 => "참여 중인 스터디" 버튼 */} - {/* 스터디 진행중 => "참여 중인 스터디" disabled */} - {/* 스터디 종료 => "신청하기" disabled */} - {/* 스터디 강퇴 => "신청하기" disabled */} - {!isLeader && isLoggedIn && ( - = maxMembersCount || - !isLoggedIn - } - > - {myApplicationStatus?.status === 'APPROVED' || - groupStudyStatus === 'IN_PROGRESS' - ? '참여 중인 스터디' - : '신청하기'} - - } - /> - )} - {!isLoggedIn && ( - - )} - - -
    -
    - ); -} diff --git a/src/features/study/ui/study-card.tsx b/src/features/study/ui/study-card.tsx new file mode 100644 index 00000000..4d01e97e --- /dev/null +++ b/src/features/study/ui/study-card.tsx @@ -0,0 +1,148 @@ +'use client'; + +import { Clock5, User, Users } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; +import Badge from '@/components/ui/badge'; + +import { GroupStudyData, StudyType } from '../group/api/group-study-types'; +import { + REGULAR_MEETING_LABELS, + STUDY_TYPE_LABELS, +} from '../group/const/group-study-const'; + +type BadgeColor = + | 'default' + | 'primary' + | 'green' + | 'red' + | 'blue' + | 'orange' + | 'gray' + | 'purple'; + +const STUDY_TYPE_BADGE_COLORS: Record = { + PROJECT: 'red', + MENTORING: 'blue', + SEMINAR: 'green', + CHALLENGE: 'orange', + BOOK_STUDY: 'purple', + LECTURE_STUDY: 'primary', +}; + +interface StudyCardProps { + study: GroupStudyData; + href: string; + onClick?: () => void; +} + +export default function StudyCard({ study, href, onClick }: StudyCardProps) { + const studyType = study.basicInfo.type; + const badgeColor = STUDY_TYPE_BADGE_COLORS[studyType]; + const price = study.basicInfo.price; + + return ( + + {/* 썸네일 영역 */} +
    + {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] + ?.resizedImageUrl ? ( + {study.simpleDetailInfo.title} + ) : ( +
    + ZERO ONE IT +
    + )} +
    + + {/* 컨텐츠 영역 */} +
    + {/* 뱃지 */} +
    + {STUDY_TYPE_LABELS[studyType]} +
    + + {/* 제목 */} +

    + {study.simpleDetailInfo.title} +

    + + {/* 설명 */} +

    + {study.simpleDetailInfo.summary} +

    + + {/* 하단 정보 */} +
    +
    + + + {study.basicInfo.maxMembersCount}명 + +
    +
    + + + {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} + +
    +
    + + {/* 리더 정보 & 가격 */} +
    +
    +
    + {study.basicInfo.leader?.profileImage?.resizedImages?.[0] + ?.resizedImageUrl ? ( + 프로필 + ) : ( + 프로필 + )} +
    +
    +

    + {study.basicInfo.leader?.memberName || '스터디장'} +

    +

    스터디장

    +
    +
    + + {/* 가격 (0원이면 숨김) */} + {price > 0 && ( + + {price.toLocaleString()} + + 원 + + + )} +
    +
    + + ); +} diff --git a/src/features/study/ui/summary-study-info.tsx b/src/features/study/ui/summary-study-info.tsx new file mode 100644 index 00000000..1e35d1ba --- /dev/null +++ b/src/features/study/ui/summary-study-info.tsx @@ -0,0 +1,253 @@ +'use client'; + +import dayjs from 'dayjs'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import Button from '@/components/ui/button'; +import { GroupStudyDetailResponse } from '../group/api/group-study-types'; +import { + EXPERIENCE_LEVEL_LABELS, + REGULAR_MEETING_LABELS, + ROLE_LABELS, + STUDY_METHOD_LABELS, + STUDY_STATUS_LABELS, + STUDY_TYPE_LABELS, +} from '../group/const/group-study-const'; +import { useGroupStudyMyStatusQuery } from '../group/model/use-group-study-my-status-query'; + +interface Props { + data: GroupStudyDetailResponse; + memberId?: number; +} + +export default function SummaryStudyInfo({ data, memberId }: Props) { + const router = useRouter(); + const [isExpanded, setIsExpanded] = useState(false); + + const { basicInfo, detailInfo } = data; + const { + groupStudyId, + hostType, + status: groupStudyStatus, + maxMembersCount, + approvedCount, + price, + leader, + method, + startDate, + endDate, + regularMeeting, + location, + type, + targetRoles, + experienceLevels, + } = basicInfo; + const { title } = detailInfo; + + const isLeader = leader.memberId === memberId; + const isLoggedIn = typeof memberId === 'number'; + const isPremium = hostType === 'ZEROONE' || hostType === 'METOR'; + + const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ + groupStudyId, + isLeader, + }); + + const getDurationText = (start: string, end: string): string => { + const startDateObj = new Date(start); + const endDateObj = new Date(end); + + const diffTime = endDateObj.getTime() - startDateObj.getTime(); + if (diffTime < 0) return '기간이 잘못되었습니다.'; + + const diffDays = diffTime / (1000 * 60 * 60 * 24); + const diffWeeks = diffDays / 7; + const diffMonths = diffDays / 30; + + return diffMonths < 1 + ? `약 ${Math.round(diffWeeks)}주` + : `약 ${Math.round(diffMonths)}개월`; + }; + + const infoItems = [ + { label: '유형', value: STUDY_TYPE_LABELS[type] }, + { + label: '주제', + value: targetRoles.map((role) => ROLE_LABELS[role]).join(', '), + }, + { + label: '진행 방식', + value: STUDY_METHOD_LABELS[method], + }, + { + label: '상태', + value: STUDY_STATUS_LABELS[groupStudyStatus], + }, + { + label: '현직자 참여 여부', + value: + experienceLevels + .map((level) => EXPERIENCE_LEVEL_LABELS[level]) + .join(', ') || '무관', + }, + { + label: '진행 기간', + value: getDurationText(startDate, endDate), + }, + { + label: '정기모임 유무', + value: `${REGULAR_MEETING_LABELS[regularMeeting]}${location ? `, ${location}` : ''}`, + }, + { + label: '모집인원', + value: `${maxMembersCount}명`, + }, + { + label: '스터디 기간', + value: `${dayjs(startDate).format('YYYY.MM.DD')} ~ ${dayjs(endDate).format('YYYY.MM.DD')}`, + }, + { + label: '참가비', + value: price === 0 ? '무료' : `${price.toLocaleString()}원`, + }, + ]; + + const visibleItems = isExpanded ? infoItems : infoItems.slice(0, 4); + + const handleCopyURL = async () => { + await navigator.clipboard.writeText(window.location.href); + alert('스터디 링크가 복사되었습니다!'); + }; + + const handleApplyClick = () => { + router.push(`/payment/${groupStudyId}`); + }; + + const isApplyDisabled = + myApplicationStatus?.status !== 'NONE' || + groupStudyStatus === 'IN_PROGRESS' || + approvedCount >= maxMembersCount; + + const getButtonText = () => { + if ( + myApplicationStatus?.status === 'APPROVED' || + groupStudyStatus === 'IN_PROGRESS' + ) { + return '참여 중인 스터디'; + } + if (myApplicationStatus?.status === 'PENDING') { + return '승인 대기중'; + } + + return '신청하기'; + }; + + return ( +
    + {/* 제목 */} +

    {title}

    + + {/* 정보 리스트 */} +
    + {visibleItems.map((item) => ( +
    + + {item.label} + + + {item.value} + +
    + ))} +
    + +
    + + {/* 더보기/접기 버튼 */} + {infoItems.length > 4 && ( + + )} + + {/* 버튼 영역 */} +
    + {/* 프리미엄(유료) 스터디: 결제 페이지로 이동 */} + {isPremium && !isLeader && isLoggedIn && ( + + )} + + {/* 무료 스터디: 모달로 신청 */} + {/* {!isPremium && !isLeader && isLoggedIn && ( + + {getButtonText()} + + } + /> + )} */} + + {/* 비로그인 상태 */} + {!isLoggedIn && ( + + )} + + +
    +
    + ); +} diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index 9971d3c0..be2414a0 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -13,6 +13,8 @@ export default async function Sidebar() { const userProfile = await getUserProfileInServer(memberId); + console.log('userProfile', userProfile); + return (
  • ))} @@ -170,9 +150,9 @@ export default function NotificationPage() { {/* Pagination */}
    diff --git a/src/hooks/queries/notification-api.ts b/src/hooks/queries/notification-api.ts index f365b993..388ba396 100644 --- a/src/hooks/queries/notification-api.ts +++ b/src/hooks/queries/notification-api.ts @@ -62,7 +62,7 @@ export const useReadNotifications = () => { mutationFn: async (ids?: number[]) => { const { data } = await notificationApi.readMemberNotifications(ids); - return data.content; + // return data.content; }, onSuccess: async () => { await Promise.all([ From 7b5a549caa71abce1ad111b44874bedecf6e4445 Mon Sep 17 00:00:00 2001 From: aken-you Date: Sun, 28 Dec 2025 16:03:53 +0900 Subject: [PATCH 017/211] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(service)/(my)/notification/page.tsx | 47 +-------------- src/components/lists/notification-list.tsx | 60 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 45 deletions(-) create mode 100644 src/components/lists/notification-list.tsx diff --git a/src/app/(service)/(my)/notification/page.tsx b/src/app/(service)/(my)/notification/page.tsx index 20e43497..b0f085ba 100644 --- a/src/app/(service)/(my)/notification/page.tsx +++ b/src/app/(service)/(my)/notification/page.tsx @@ -1,10 +1,9 @@ 'use client'; -import { format } from 'date-fns'; import { useState } from 'react'; import type { GetMemberNotificationsTopicTypeEnum } from '@/api/openapi/api/notification-api'; -import Badge from '@/components/ui/badge'; +import NotificationList from '@/components/lists/notification-list'; import Button from '@/components/ui/button'; import SingleDropdown from '@/components/ui/dropdown/single'; import Pagination from '@/components/ui/pagination'; @@ -21,23 +20,6 @@ const READ_STATUS_OPTIONS = [ { value: 'unread', label: '안 읽음' }, ]; -const getBadgeColor = ( - topicType: string, -): 'primary' | 'green' | 'red' | 'blue' | 'orange' | 'gray' | 'purple' => { - switch (topicType) { - case 'ONE_ON_ONE_STUDY': - return 'blue'; - case 'GROUP_STUDY': - return 'purple'; - case 'PAYMENT': - return 'green'; - case 'ETC': - return 'gray'; - default: - return 'gray'; - } -}; - export default function NotificationPage() { const [page, setPage] = useState(1); const [category, setCategory] = @@ -120,32 +102,7 @@ export default function NotificationPage() { - {/* Notification List */} -
      - {notifications?.map((notification) => ( -
    • -
      - - {notification.topicDescription} - - - {notification.title} - -
      - - {format(notification.createdAt, 'yyyy.MM.dd HH:mm')} - -
    • - ))} -
    + {/* Pagination */}
    diff --git a/src/components/lists/notification-list.tsx b/src/components/lists/notification-list.tsx new file mode 100644 index 00000000..5670b155 --- /dev/null +++ b/src/components/lists/notification-list.tsx @@ -0,0 +1,60 @@ +import { format } from 'date-fns'; +import { MemberNotificationResponse } from '@/api/openapi'; +import Badge from '../ui/badge'; + +const getBadgeColor = ( + topicType: string, +): 'primary' | 'green' | 'red' | 'blue' | 'orange' | 'gray' | 'purple' => { + switch (topicType) { + case 'ONE_ON_ONE_STUDY': + return 'blue'; + case 'GROUP_STUDY': + return 'purple'; + case 'PAYMENT': + return 'green'; + case 'MENTOR_ADMITTANCE': + return 'red'; + case 'SYSTEM': + return 'gray'; + case 'MARKETING': + return 'orange'; + default: + return 'gray'; + } +}; + +interface NotificationListProps { + notifications?: MemberNotificationResponse[]; +} + +export default function NotificationList({ + notifications, +}: NotificationListProps) { + return ( +
      + {notifications?.map((notification) => ( +
    • +
      + + {notification.topicDescription} + + + {notification.title} + +
      + + {format(notification.createdAt, 'yyyy.MM.dd HH:mm')} + +
    • + ))} +
    + ); +} From 521b25e152227c77f728235d2344b932c91e3c66 Mon Sep 17 00:00:00 2001 From: aken-you Date: Sun, 28 Dec 2025 16:19:25 +0900 Subject: [PATCH 018/211] =?UTF-8?q?feat:=20=ED=97=A4=EB=8D=94=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modals/notification-dropdown.tsx | 91 +++++++++++++++++++ src/widgets/home/header.tsx | 10 +- 2 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 src/components/modals/notification-dropdown.tsx diff --git a/src/components/modals/notification-dropdown.tsx b/src/components/modals/notification-dropdown.tsx new file mode 100644 index 00000000..5c93cb11 --- /dev/null +++ b/src/components/modals/notification-dropdown.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useState } from 'react'; +import NotificationList from '@/components/lists/notification-list'; + +import { useGetNotifications } from '@/hooks/queries/notification-api'; +import NotiIcon from 'public/icons/notifications_none.svg'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from '../ui/(shadcn)/ui/dropdown-menu'; +import Button from '../ui/button'; + +export default function NotificationDropdown() { + const [mode, setMode] = useState<'all' | 'unread'>('all'); + const { data: notificationsData } = useGetNotifications({ + page: 1, + size: 5, + }); + const { data: notReadNotificationsData } = useGetNotifications({ + page: 1, + size: 5, + hasRead: false, + }); + + const notifications = + mode === 'all' + ? notificationsData?.content || [] + : notReadNotificationsData?.content || []; + + const totalAllCount = notificationsData?.totalElements; + const totalUnreadCount = notReadNotificationsData?.totalElements; + + return ( + + + + + + {/* Header */} +
    + + +
    + + {/* Notification List */} +
    + {notifications.length === 0 ? ( +
    +

    + 새로운 알림이 없습니다 +

    +
    + ) : ( + + )} +
    + + {/* Footer */} +
    + +
    +
    +
    + ); +} diff --git a/src/widgets/home/header.tsx b/src/widgets/home/header.tsx index 9ad20748..ec8276a1 100644 --- a/src/widgets/home/header.tsx +++ b/src/widgets/home/header.tsx @@ -1,12 +1,12 @@ import Image from 'next/image'; import Link from 'next/link'; +import NotificationDropdown from '@/components/modals/notification-dropdown'; import Button from '@/components/ui/button'; import { getUserProfileInServer } from '@/entities/user/api/get-user-profile.server'; import HeaderUserDropdown from '@/features/auth/ui/header-user-dropdown'; import LoginModal from '@/features/auth/ui/login-modal'; import { getServerCookie } from '@/utils/server-cookie'; import { isNumeric } from '@/utils/validation'; -import NotiIcon from 'public/icons/notifications_none.svg'; export default async function Header() { const memberIdStr = await getServerCookie('memberId'); @@ -51,14 +51,8 @@ export default async function Header() { 인사이트 - {/* 알림 기능을 구현하지 못해 주석 처리 */} + - - -
    {isLoggedIn ? ( From 9577b436033efaa3bef3b606306c581107b17828 Mon Sep 17 00:00:00 2001 From: aken-you Date: Sun, 28 Dec 2025 16:40:04 +0900 Subject: [PATCH 019/211] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(service)/(my)/notification/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(service)/(my)/notification/page.tsx b/src/app/(service)/(my)/notification/page.tsx index b0f085ba..ffa3e4a7 100644 --- a/src/app/(service)/(my)/notification/page.tsx +++ b/src/app/(service)/(my)/notification/page.tsx @@ -56,7 +56,9 @@ export default function NotificationPage() { }; const handleMarkAllAsRead = () => { - readNotifications([]); + const ids = notifications.map((notification) => notification.id); + + readNotifications(ids); }; return ( From c4c4eb07977e1f6b38a4577fd227e5f89391c37f Mon Sep 17 00:00:00 2001 From: aken-you Date: Sun, 28 Dec 2025 16:47:25 +0900 Subject: [PATCH 020/211] =?UTF-8?q?feat:=20=EC=9D=BD=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EC=95=8C=EB=A6=BC=EC=9D=B4=20=EC=9E=88=EC=9D=84=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0,=20=EB=B9=A8=EA=B0=84=EC=A0=90=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/modals/notification-dropdown.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/modals/notification-dropdown.tsx b/src/components/modals/notification-dropdown.tsx index 5c93cb11..9ca30824 100644 --- a/src/components/modals/notification-dropdown.tsx +++ b/src/components/modals/notification-dropdown.tsx @@ -1,9 +1,13 @@ 'use client'; +import { DotIcon } from 'lucide-react'; import { useState } from 'react'; import NotificationList from '@/components/lists/notification-list'; -import { useGetNotifications } from '@/hooks/queries/notification-api'; +import { + useGetNotifications, + useHasNewNotification, +} from '@/hooks/queries/notification-api'; import NotiIcon from 'public/icons/notifications_none.svg'; import { DropdownMenu, @@ -23,6 +27,9 @@ export default function NotificationDropdown() { size: 5, hasRead: false, }); + const { data: newData } = useHasNewNotification(); + + const isRead = newData?.isRead; const notifications = mode === 'all' @@ -35,7 +42,13 @@ export default function NotificationDropdown() { return ( - +
    + + + {!isRead && ( + + )} +
    {/* Header */} From ff56dab0e302832901e7ca04988b55a827508d4e Mon Sep 17 00:00:00 2001 From: aken-you Date: Sun, 28 Dec 2025 16:55:36 +0900 Subject: [PATCH 021/211] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20dropdown?= =?UTF-8?q?=20=ED=83=AD=20=EB=B2=84=ED=8A=BC=20cursor-pointer=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/modals/notification-dropdown.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/modals/notification-dropdown.tsx b/src/components/modals/notification-dropdown.tsx index 9ca30824..8d455c31 100644 --- a/src/components/modals/notification-dropdown.tsx +++ b/src/components/modals/notification-dropdown.tsx @@ -56,8 +56,8 @@ export default function NotificationDropdown() {
    diff --git a/src/app/(service)/premium-study/[id]/page.tsx b/src/app/(service)/premium-study/[id]/page.tsx index e7c8a752..fed6e93a 100644 --- a/src/app/(service)/premium-study/[id]/page.tsx +++ b/src/app/(service)/premium-study/[id]/page.tsx @@ -7,10 +7,10 @@ import type { Metadata } from 'next'; import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; import { Configuration } from '@/api/openapi/configuration'; import type { GroupStudyFullResponseDto } from '@/api/openapi/models'; +import PremiumStudyDetailPage from '@/components/premium/premium-study-detail-page'; import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; import { getGroupStudyMyStatusInServer } from '@/features/study/group/api/get-group-study-my-status.server'; import { GroupStudyDetailResponse } from '@/features/study/group/api/group-study-types'; -import PremiumStudyDetailPage from '@/features/study/premium/ui/premium-study-detail-page'; import { getServerCookie } from '@/utils/server-cookie'; interface Props { @@ -103,10 +103,7 @@ export default async function Page({ return ( - + ); } diff --git a/src/app/(service)/premium-study/page.tsx b/src/app/(service)/premium-study/page.tsx index b2b8ecfc..7a7bd1c8 100644 --- a/src/app/(service)/premium-study/page.tsx +++ b/src/app/(service)/premium-study/page.tsx @@ -1,10 +1,10 @@ import { Plus } from 'lucide-react'; -import { createApiServerInstance } from '@/api/client/open-api-instance'; +import { createApiServerInstance } from '@/api/client/open-api-instance.server'; import { GroupStudyManagementApi } from '@/api/openapi/api/group-study-management-api'; +import PremiumStudyList from '@/components/premium/premium-study-list'; +import PremiumStudyPagination from '@/components/premium/premium-study-pagination'; import Button from '@/components/ui/button'; import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-modal'; -import PremiumStudyList from '@/features/study/premium/ui/premium-study-list'; -import PremiumStudyPagination from '@/features/study/premium/ui/premium-study-pagination'; interface PremiumStudyPageProps { searchParams: Promise<{ page?: string }>; diff --git a/src/features/study/premium/ui/premium-study-detail-page.tsx b/src/components/premium/premium-study-detail-page.tsx similarity index 91% rename from src/features/study/premium/ui/premium-study-detail-page.tsx rename to src/components/premium/premium-study-detail-page.tsx index 81a7c42e..19a423dc 100644 --- a/src/features/study/premium/ui/premium-study-detail-page.tsx +++ b/src/components/premium/premium-study-detail-page.tsx @@ -6,17 +6,17 @@ import { useEffect, useState } from 'react'; import MoreMenu from '@/components/ui/dropdown/more-menu'; import Tabs from '@/components/ui/tabs'; import { useLeaderStore } from '@/stores/useLeaderStore'; -import ConfirmDeleteModal from '../../group/ui/confirm-delete-modal'; -import GroupStudyFormModal from '../../group/ui/group-study-form-modal'; -import GroupStudyMemberList from '../../group/ui/group-study-member-list'; +import ConfirmDeleteModal from '../../features/study/group/ui/confirm-delete-modal'; +import GroupStudyFormModal from '../../features/study/group/ui/group-study-form-modal'; +import GroupStudyMemberList from '../../features/study/group/ui/group-study-member-list'; import PremiumStudyInfoSection from './premium-study-info-section'; -import ChannelSection from '../../group/channel/ui/channel-section'; -import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; +import ChannelSection from '../../features/study/group/channel/ui/channel-section'; +import { useGroupStudyMyStatusQuery } from '../../features/study/group/model/use-group-study-my-status-query'; import { useCompleteGroupStudyMutation, useDeleteGroupStudyMutation, useGroupStudyDetailQuery, -} from '../../group/model/use-study-query'; +} from '../../features/study/group/model/use-study-query'; type ActiveTab = 'intro' | 'members' | 'channel'; @@ -212,12 +212,7 @@ export default function PremiumStudyDetailPage({ }} /> {active === 'intro' && ( - + )} {active === 'members' && ( (pages: { content: T[] }[] | undefined) { + if (!pages) return []; + + return pages.reduce((acc, page) => [...acc, ...page.content], []); +} + +interface PremiumStudyInfoSectionProps { + study: GroupStudyDetailResponse; + isLeader: boolean; +} + +export default function PremiumStudyInfoSection({ + study: studyDetail, + isLeader, +}: PremiumStudyInfoSectionProps) { + const router = useRouter(); + const params = useParams(); + const { data: authData } = useAuth(); + + const groupStudyId = Number(params.id); + + const { data: approvedApplicants } = useApplicantsByStatusQuery({ + groupStudyId, + status: 'APPROVED', + }); + + const applicantsList = getApplicantsList(approvedApplicants?.pages); + + return ( +
    +
    +
    + 썸네일 +
    + +
    +
    +

    스터디 소개

    +
    +
    + +
    +
    + + {studyDetail.basicInfo.leader.memberName} + +
    + 스터디 리더 + + + {studyDetail.basicInfo.leader.simpleIntroduction} + +
    +
    +
    +
    + + 프로필 +
    + } + /> +
    +
    + {studyDetail?.detailInfo.description} +
    +
    + +
    +
    +
    + 실시간 신청자 목록 + {`${applicantsList.length}명`} +
    + {isLeader && ( + + )} +
    + +
    + {applicantsList.map((data) => { + const temperPreset = getSincerityPresetByLevelName( + data.applicantInfo.sincerityTemp.levelName as string, + ); + + return ( +
    + +
    +
    +
    + {data.applicantInfo.memberNickname !== '' + ? data.applicantInfo.memberNickname + : '익명'} +
    + + {`${data.applicantInfo.sincerityTemp.temperature}`}℃ + +
    +
    + { + sendGTMEvent({ + event: 'premium_study_member_profile_click', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue( + String(authData.memberId), + ), + }), + dl_target_member_id: String( + data.applicantInfo.memberId, + ), + dl_group_study_id: String(groupStudyId), + }); + }} + > + 프로필 +
    + } + /> +
    + ); + })} +
    +
    +
    + + + + ); +} diff --git a/src/components/premium/premium-study-list.tsx b/src/components/premium/premium-study-list.tsx new file mode 100644 index 00000000..dde9a5cf --- /dev/null +++ b/src/components/premium/premium-study-list.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import Image from 'next/image'; +import { useAuth } from '@/hooks/use-auth'; +import { hashValue } from '@/utils/hash'; + +import { GroupStudyData } from '../../features/study/group/api/group-study-types'; +import StudyCard from '../study/study-card'; + +interface PremiumStudyListProps { + studies: GroupStudyData[]; +} + +export default function PremiumStudyList({ studies }: PremiumStudyListProps) { + const { data: authData } = useAuth(); + + const handleStudyClick = (study: GroupStudyData) => { + sendGTMEvent({ + event: 'premium_study_detail_view', + dl_timestamp: new Date().toISOString(), + ...(authData?.memberId && { + dl_member_id: hashValue(String(authData.memberId)), + }), + dl_study_id: String(study.basicInfo.groupStudyId), + dl_study_title: study.simpleDetailInfo.title, + }); + }; + + if (studies.length === 0) { + return ( +
    + 현재 멘토 스터디가 없습니다. +
    + + 스터디가 아직 준비되지 않았습니다. + + + 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. + +
    +
    + ); + } + + return ( +
    + {studies.map((study) => ( + handleStudyClick(study)} + /> + ))} +
    + ); +} diff --git a/src/features/study/premium/ui/premium-study-pagination.tsx b/src/components/premium/premium-study-pagination.tsx similarity index 100% rename from src/features/study/premium/ui/premium-study-pagination.tsx rename to src/components/premium/premium-study-pagination.tsx diff --git a/src/features/study/ui/study-card.tsx b/src/components/study/study-card.tsx similarity index 96% rename from src/features/study/ui/study-card.tsx rename to src/components/study/study-card.tsx index 4d01e97e..0b99a927 100644 --- a/src/features/study/ui/study-card.tsx +++ b/src/components/study/study-card.tsx @@ -5,11 +5,14 @@ import Image from 'next/image'; import Link from 'next/link'; import Badge from '@/components/ui/badge'; -import { GroupStudyData, StudyType } from '../group/api/group-study-types'; +import { + GroupStudyData, + StudyType, +} from '../../features/study/group/api/group-study-types'; import { REGULAR_MEETING_LABELS, STUDY_TYPE_LABELS, -} from '../group/const/group-study-const'; +} from '../../features/study/group/const/group-study-const'; type BadgeColor = | 'default' diff --git a/src/features/study/ui/summary-study-info.tsx b/src/components/study/summary-study-info.tsx similarity index 74% rename from src/features/study/ui/summary-study-info.tsx rename to src/components/study/summary-study-info.tsx index 1e35d1ba..f43e0acd 100644 --- a/src/features/study/ui/summary-study-info.tsx +++ b/src/components/study/summary-study-info.tsx @@ -5,7 +5,8 @@ import { ChevronDown, ChevronUp } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import Button from '@/components/ui/button'; -import { GroupStudyDetailResponse } from '../group/api/group-study-types'; +import ApplyGroupStudyModal from '@/features/study/group/ui/apply-group-study-modal'; +import { GroupStudyDetailResponse } from '../../features/study/group/api/group-study-types'; import { EXPERIENCE_LEVEL_LABELS, REGULAR_MEETING_LABELS, @@ -13,8 +14,8 @@ import { STUDY_METHOD_LABELS, STUDY_STATUS_LABELS, STUDY_TYPE_LABELS, -} from '../group/const/group-study-const'; -import { useGroupStudyMyStatusQuery } from '../group/model/use-group-study-my-status-query'; +} from '../../features/study/group/const/group-study-const'; +import { useGroupStudyMyStatusQuery } from '../../features/study/group/model/use-group-study-my-status-query'; interface Props { data: GroupStudyDetailResponse; @@ -25,7 +26,7 @@ export default function SummaryStudyInfo({ data, memberId }: Props) { const router = useRouter(); const [isExpanded, setIsExpanded] = useState(false); - const { basicInfo, detailInfo } = data; + const { basicInfo, detailInfo, interviewPost } = data; const { groupStudyId, hostType, @@ -44,6 +45,8 @@ export default function SummaryStudyInfo({ data, memberId }: Props) { experienceLevels, } = basicInfo; const { title } = detailInfo; + const { interviewPost: questions } = interviewPost; + console.log('interviewPost', interviewPost); const isLeader = leader.memberId === memberId; const isLoggedIn = typeof memberId === 'number'; @@ -54,6 +57,8 @@ export default function SummaryStudyInfo({ data, memberId }: Props) { isLeader, }); + console.log('myApplicationStatus', myApplicationStatus); + const getDurationText = (start: string, end: string): string => { const startDateObj = new Date(start); const endDateObj = new Date(end); @@ -120,8 +125,10 @@ export default function SummaryStudyInfo({ data, memberId }: Props) { alert('스터디 링크가 복사되었습니다!'); }; - const handleApplyClick = () => { - router.push(`/payment/${groupStudyId}`); + const handleApplySuccess = () => { + if (price > 0) { + router.push(`/payment/${groupStudyId}`); + } }; const isApplyDisabled = @@ -187,57 +194,24 @@ export default function SummaryStudyInfo({ data, memberId }: Props) { {/* 버튼 영역 */}
    - {/* 프리미엄(유료) 스터디: 결제 페이지로 이동 */} - {isPremium && !isLeader && isLoggedIn && ( - - )} - - {/* 무료 스터디: 모달로 신청 */} - {/* {!isPremium && !isLeader && isLoggedIn && ( - - {getButtonText()} - - } - /> - )} */} - - {/* 비로그인 상태 */} - {!isLoggedIn && ( - - )} + {/* 스터디 신청 모달 (유료/무료 공통) */} + + + {getButtonText()} + + } + /> - )} -
    - -
    - {applicants.map((data) => { - const temperPreset = getSincerityPresetByLevelName( - data.applicantInfo.sincerityTemp.levelName as string, - ); - - return ( -
    - -
    -
    -
    - {data.applicantInfo.memberNickname !== '' - ? data.applicantInfo.memberNickname - : '익명'} -
    - - {`${data.applicantInfo.sincerityTemp.temperature}`}℃ - -
    -
    - { - sendGTMEvent({ - event: 'premium_study_member_profile_click', - dl_timestamp: new Date().toISOString(), - ...(authData?.memberId && { - dl_member_id: hashValue( - String(authData.memberId), - ), - }), - dl_target_member_id: String( - data.applicantInfo.memberId, - ), - dl_group_study_id: String(groupStudyId), - }); - }} - > - 프로필 -
    - } - /> -
    - ); - })} - - - - - - - ); -} diff --git a/src/features/study/premium/ui/premium-study-list.tsx b/src/features/study/premium/ui/premium-study-list.tsx deleted file mode 100644 index 5c18244a..00000000 --- a/src/features/study/premium/ui/premium-study-list.tsx +++ /dev/null @@ -1,159 +0,0 @@ -'use client'; - -import { sendGTMEvent } from '@next/third-parties/google'; -import Image from 'next/image'; -import Link from 'next/link'; -import Badge from '@/components/ui/badge'; -import Button from '@/components/ui/button'; -import { useAuth } from '@/hooks/use-auth'; -import { hashValue } from '@/utils/hash'; - -import { GroupStudyData } from '../../group/api/group-study-types'; -import { REGULAR_MEETING_LABELS } from '../../group/const/group-study-const'; - -interface PremiumStudyListProps { - studies: GroupStudyData[]; -} - -export default function PremiumStudyList({ studies }: PremiumStudyListProps) { - const { data: authData } = useAuth(); - - const handleStudyClick = (study: GroupStudyData) => { - sendGTMEvent({ - event: 'premium_study_detail_view', - dl_timestamp: new Date().toISOString(), - ...(authData?.memberId && { - dl_member_id: hashValue(String(authData.memberId)), - }), - dl_study_id: String(study.basicInfo.groupStudyId), - dl_study_title: study.simpleDetailInfo.title, - }); - }; - - if (studies.length === 0) { - return ( -
    - 현재 멘토 스터디가 없습니다. -
    - - 스터디가 아직 준비되지 않았습니다. - - - 원하는 주제로 스터디를 개설해 첫 번째 참여자가 되어보세요. - -
    -
    - ); - } - - return ( -
    - {studies.map((study) => ( - handleStudyClick(study)} - className="cursor-pointer overflow-hidden rounded-[12px] border border-[#E5E7EB] bg-white transition-shadow hover:shadow-md" - > - {/* 썸네일 영역 */} -
    - {study.simpleDetailInfo.thumbnail?.resizedImages?.[0] - ?.resizedImageUrl ? ( - {study.simpleDetailInfo.title} - ) : ( -
    - ZERO ONE IT -
    - )} - {study.basicInfo.hostType === 'ZEROONE' && ( -
    - 제로원 스터디 -
    - )} -
    - - {/* 컨텐츠 영역 */} -
    - {/* 제목 */} -

    - {study.simpleDetailInfo.title} -

    - - {/* 설명 */} -

    - {study.simpleDetailInfo.summary} -

    - - {/* 리더 정보 */} -
    -
    -
    - {study.basicInfo.leader?.profileImage?.resizedImages?.[0] - ?.resizedImageUrl ? ( - 프로필 - ) : ( - 프로필 - )} -
    -
    -

    - {study.basicInfo.leader?.memberName || '스터디장'} -

    -

    스터디장

    -
    -
    -
    - - {study.basicInfo.approvedCount}/ - {study.basicInfo.maxMembersCount}명 - - - {REGULAR_MEETING_LABELS[study.basicInfo.regularMeeting]} - -
    -
    - - {/* 가격 및 버튼 */} -
    - - {study.basicInfo.price === 0 - ? '무료' - : `${study.basicInfo.price.toLocaleString()}원`} - - -
    -
    - - ))} -
    - ); -} diff --git a/src/features/study/premium/ui/premium-summary-study-info.tsx b/src/features/study/premium/ui/premium-summary-study-info.tsx deleted file mode 100644 index 3fa717bc..00000000 --- a/src/features/study/premium/ui/premium-summary-study-info.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import React from 'react'; -import Button from '@/components/ui/button'; -import { - GroupStudyDetailResponse, - GroupStudyStatus, -} from '../../group/api/group-study-types'; -import { useGroupStudyMyStatusQuery } from '../../group/model/use-group-study-my-status-query'; - -interface PremiumSummaryStudyInfoProps { - data: { - label: string; - value: string; - icon: React.ReactNode; - }[]; - title: string; - groupStudyId: number; - questions: GroupStudyDetailResponse['interviewPost']['interviewPost']; - isLeader: boolean; - groupStudyStatus: GroupStudyStatus; - approvedCount: GroupStudyDetailResponse['basicInfo']['approvedCount']; - maxMembersCount: GroupStudyDetailResponse['basicInfo']['maxMembersCount']; - price: number; - memberId?: number; -} - -export default function PremiumSummaryStudyInfo({ - data, - title, - groupStudyId, - isLeader, - groupStudyStatus, - approvedCount, - maxMembersCount, - price, - memberId, -}: PremiumSummaryStudyInfoProps) { - const router = useRouter(); - const { data: myApplicationStatus } = useGroupStudyMyStatusQuery({ - groupStudyId, - isLeader, - }); - - const isLoggedIn = typeof memberId === 'number'; - - const handleCopyURL = async () => { - await navigator.clipboard.writeText(window.location.href); - alert('스터디 링크가 복사되었습니다!'); - }; - - const handleApplyClick = () => { - // 결제 페이지로 이동 (groupStudyId를 전달) - router.push(`/payment/${groupStudyId}`); - }; - - const isApplyDisabled = - myApplicationStatus?.status !== 'NONE' || - groupStudyStatus === 'IN_PROGRESS' || - approvedCount >= maxMembersCount; - - const getButtonText = () => { - if ( - myApplicationStatus?.status === 'APPROVED' || - groupStudyStatus === 'IN_PROGRESS' - ) { - return '참여 중인 스터디'; - } - if (myApplicationStatus?.status === 'PENDING') { - return '승인 대기중'; - } - return '신청하기'; - }; - - return ( -
    -

    {title}

    -
    -
    - {data.map((item) => ( -
    -
    {item.icon}
    - - {item.value} - -
    - ))} -
    - - {/* 가격 표시 */} -
    - 참가비 - - {price === 0 ? '무료' : `${price.toLocaleString()}원`} - -
    - -
    - {!isLeader && isLoggedIn && ( - - )} - {!isLoggedIn && ( - - )} - - -
    -
    - ); -} From 661ded34794501259386604616220e818cad8a18 Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 29 Dec 2025 00:37:51 +0900 Subject: [PATCH 024/211] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=82=AC=EB=9E=8C=EB=A7=8C=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?api=20=EC=9A=94=EC=B2=AD=20=EB=B3=B4=EB=82=B4=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/queries/notification-api.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/hooks/queries/notification-api.ts b/src/hooks/queries/notification-api.ts index a8e18d08..ef3ba516 100644 --- a/src/hooks/queries/notification-api.ts +++ b/src/hooks/queries/notification-api.ts @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { createApiInstance } from '@/api/client/open-api-instance'; import { NotificationApi } from '@/api/openapi'; import type { GetMemberNotificationsTopicTypeEnum } from '@/api/openapi/api/notification-api'; +import { useAuth } from '../use-auth'; const notificationApi = createApiInstance(NotificationApi); @@ -18,6 +19,8 @@ export const useGetNotifications = ({ hasRead, topicType, }: NotificationsParams = {}) => { + const { isAuthenticated } = useAuth(); + return useQuery({ queryKey: ['notifications', page, size, hasRead, topicType], queryFn: async () => { @@ -31,10 +34,13 @@ export const useGetNotifications = ({ return data.content; }, refetchInterval: 60000, // 1 minute + enabled: !!isAuthenticated, }); }; export const useGetNotificationCategories = () => { + const { isAuthenticated } = useAuth(); + return useQuery({ queryKey: ['notificationCategories'], queryFn: async () => { @@ -42,10 +48,13 @@ export const useGetNotificationCategories = () => { return data.content; }, + enabled: !!isAuthenticated, }); }; export const useHasNewNotification = () => { + const { isAuthenticated } = useAuth(); + return useQuery({ queryKey: ['hasNewNotification'], queryFn: async () => { @@ -54,6 +63,7 @@ export const useHasNewNotification = () => { return data.content; }, refetchInterval: 60000, // 1 minute + enabled: !!isAuthenticated, }); }; From a265ac97b56e52e7cd8569b0e523d7d84eff634b Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 29 Dec 2025 00:40:10 +0900 Subject: [PATCH 025/211] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EB=A7=8C?= =?UTF-8?q?=20=EC=83=81=EB=8B=A8=20=EC=95=8C=EB=A6=BC=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/home/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/home/header.tsx b/src/widgets/home/header.tsx index ec8276a1..de45c5a1 100644 --- a/src/widgets/home/header.tsx +++ b/src/widgets/home/header.tsx @@ -51,7 +51,7 @@ export default async function Header() { 인사이트 - + {accessTokenStr && }
    {isLoggedIn ? ( From 6aa226ad063568357a64bbf44485c550cf0eaf1c Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 29 Dec 2025 00:41:17 +0900 Subject: [PATCH 026/211] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EB=AA=A9=EB=A1=9D=20=ED=81=B4=EB=A6=AD=ED=95=98?= =?UTF-8?q?=EB=A9=B4=20=EC=95=8C=EB=A6=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/modals/notification-dropdown.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/modals/notification-dropdown.tsx b/src/components/modals/notification-dropdown.tsx index 8d455c31..53245faa 100644 --- a/src/components/modals/notification-dropdown.tsx +++ b/src/components/modals/notification-dropdown.tsx @@ -1,6 +1,7 @@ 'use client'; import { DotIcon } from 'lucide-react'; +import { useRouter } from 'next/navigation'; import { useState } from 'react'; import NotificationList from '@/components/lists/notification-list'; @@ -17,6 +18,8 @@ import { import Button from '../ui/button'; export default function NotificationDropdown() { + const router = useRouter(); + const [mode, setMode] = useState<'all' | 'unread'>('all'); const { data: notificationsData } = useGetNotifications({ page: 1, @@ -94,6 +97,7 @@ export default function NotificationDropdown() { color="outlined" size="medium" className="font-designer-16m w-full" + onClick={() => router.push('/notification')} > 전체 알림 목록 From d29d334e0485e9110164bdb4d1984603b72cd435 Mon Sep 17 00:00:00 2001 From: kinsk2839 Date: Mon, 29 Dec 2025 00:44:03 +0900 Subject: [PATCH 027/211] =?UTF-8?q?chore:=20sprint2=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=97=B4=EB=A6=AC=EB=8A=94=20PR=EC=97=90=EC=84=9C=20CI=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 533caede..c44463c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: branches: - develop - main + - sprint2 jobs: lint-type-build: From 2283b42fe69f99dd45d735c8a74938260c12727d Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 29 Dec 2025 01:53:57 +0000 Subject: [PATCH 028/211] feat: new OpenAPI (automated) --- src/api/openapi/.openapi-generator/FILES | 100 ++++++++++++------ src/api/openapi/api/admin-api.ts | 6 +- src/api/openapi/api/admin-matching-api.ts | 14 +-- src/api/openapi/api/auth-api.ts | 8 +- src/api/openapi/api/daily-study-api.ts | 2 + src/api/openapi/api/file-controller-api.ts | 6 +- .../openapi/api/group-study-homework-api.ts | 12 ++- src/api/openapi/api/group-study-member-api.ts | 6 +- src/api/openapi/api/matching-api.ts | 2 + .../openapi/api/member-tmp-controller-api.ts | 6 +- src/api/openapi/api/settlement-account-api.ts | 6 +- src/api/openapi/api/study-space-api.ts | 4 +- src/api/openapi/api/token-apiapi.ts | 6 +- src/api/openapi/docs/AdminApi.md | 4 +- src/api/openapi/docs/AdminMatchingApi.md | 12 +-- src/api/openapi/docs/AuthApi.md | 4 +- .../docs/BaseResponseGrantedTokenInfo.md | 26 +++++ ...esponseGroupStudyMembersResponseContent.md | 26 +++++ ...esponseGroupStudyProgressGradesResponse.md | 26 +++++ .../BaseResponseHomeworkSubmissionResponse.md | 26 +++++ .../BaseResponseMatchingRequestResponse.md | 26 +++++ ...ResponseMemberAccountHistoryResponseDto.md | 26 +++++ src/api/openapi/docs/BaseResponseObject.md | 26 +++++ src/api/openapi/docs/BaseResponseString.md | 26 +++++ ...ResponseStudySpaceIsParticipateResponse.md | 26 +++++ .../openapi/docs/BaseResponseTemporalToken.md | 26 +++++ src/api/openapi/docs/BaseResponseVoid.md | 26 +++++ src/api/openapi/docs/ChangeHistDto.md | 24 +++++ ...orResponseDtoStudyReservationMemberDto.md} | 8 +- src/api/openapi/docs/FileControllerApi.md | 4 +- src/api/openapi/docs/GrantedTokenInfo.md | 32 ++++++ src/api/openapi/docs/GroupStudyApplicant.md | 2 +- .../docs/GroupStudyApplyDetailResponse.md | 2 +- ...ntent.md => GroupStudyApplyResponseDto.md} | 6 +- ...e.md => GroupStudyBasicInfoResponseDto.md} | 9 +- .../docs/GroupStudyCreationResponse.md | 2 +- ...nt.md => GroupStudyCreationResponseDto.md} | 7 +- ....md => GroupStudyDetailInfoResponseDto.md} | 9 +- .../openapi/docs/GroupStudyDetailResponse.md | 2 +- .../docs/GroupStudyDetailResponseContent.md | 25 ----- .../openapi/docs/GroupStudyFullResponseDto.md | 24 +++++ src/api/openapi/docs/GroupStudyHomeworkApi.md | 8 +- ... => GroupStudyInterviewPostResponseDto.md} | 6 +- .../GroupStudyInterviewPostUpdateResponse.md | 2 +- src/api/openapi/docs/GroupStudyListItem.md | 23 ---- src/api/openapi/docs/GroupStudyListItemDto.md | 23 ++++ .../openapi/docs/GroupStudyListResponse.md | 2 +- .../docs/GroupStudyProgressGradeResponse.md | 26 +++++ .../docs/GroupStudyProgressGradesResponse.md | 20 ++++ ....md => GroupStudySimpleInfoResponseDto.md} | 8 +- .../docs/HomeworkSubmissionResponse.md | 20 ++++ .../openapi/docs/{Image.md => ImageDto.md} | 8 +- .../openapi/docs/MatchingRequestResponse.md | 34 ++++++ .../docs/MemberAccountHistoryResponseDto.md | 30 ++++++ .../openapi/docs/MemberProfileResponseDto.md | 2 +- .../openapi/docs/MemberTmpControllerApi.md | 4 +- .../docs/MyGroupStudyApplicationResponse.md | 2 +- .../openapi/docs/MyGroupStudyApplyListItem.md | 27 ----- .../docs/MyGroupStudyApplyListItemDto.md | 27 +++++ src/api/openapi/docs/OAuth2UserInfo.md | 24 +++++ ...d => PageResponseGroupStudyListItemDto.md} | 8 +- ...geResponseMyGroupStudyApplyListItemDto.md} | 8 +- .../openapi/docs/ParticipatingStudyInfo.md | 2 +- .../{ResizedImage.md => ResizedImageDto.md} | 6 +- src/api/openapi/docs/SettlementAccountApi.md | 4 +- ...Member.md => StudyReservationMemberDto.md} | 8 +- .../docs/StudyReservationResponseContent.md | 2 +- src/api/openapi/docs/TemporalToken.md | 20 ++++ .../docs/ThreadCommentResponseContent.md | 2 +- .../docs/ThreadSummaryResponseContent.md | 2 +- src/api/openapi/docs/TokenAPIApi.md | 4 +- .../base-response-granted-token-info.ts | 38 +++++++ ...se-group-study-members-response-content.ts | 38 +++++++ ...se-group-study-progress-grades-response.ts | 38 +++++++ ...e-response-homework-submission-response.ts | 38 +++++++ ...base-response-matching-request-response.ts | 38 +++++++ ...nse-member-account-history-response-dto.ts | 38 +++++++ .../openapi/models/base-response-object.ts | 35 ++++++ .../openapi/models/base-response-string.ts | 35 ++++++ ...nse-study-space-is-participate-response.ts | 38 +++++++ .../models/base-response-temporal-token.ts | 38 +++++++ src/api/openapi/models/base-response-void.ts | 35 ++++++ src/api/openapi/models/change-hist-dto.ts | 22 ++++ ...ponse-dto-study-reservation-member-dto.ts} | 6 +- src/api/openapi/models/granted-token-info.ts | 38 +++++++ .../openapi/models/group-study-applicant.ts | 4 +- .../group-study-apply-detail-response.ts | 4 +- ...t.ts => group-study-apply-response-dto.ts} | 14 +-- ...=> group-study-basic-info-response-dto.ts} | 57 +++++----- ...s => group-study-creation-response-dto.ts} | 5 +- .../models/group-study-creation-response.ts | 4 +- ...> group-study-detail-info-response-dto.ts} | 9 +- .../models/group-study-detail-response.ts | 4 +- ...nt.ts => group-study-full-response-dto.ts} | 17 ++- ...roup-study-interview-post-response-dto.ts} | 2 +- ...up-study-interview-post-update-response.ts | 4 +- ...t-item.ts => group-study-list-item-dto.ts} | 10 +- .../models/group-study-list-response.ts | 4 +- .../group-study-progress-grade-response.ts | 23 ++++ .../group-study-progress-grades-response.ts | 23 ++++ ...> group-study-simple-info-response-dto.ts} | 6 +- .../models/homework-submission-response.ts | 20 ++++ .../openapi/models/{image.ts => image-dto.ts} | 6 +- src/api/openapi/models/index.ts | 50 ++++++--- .../models/matching-request-response.ts | 70 ++++++++++++ .../member-account-history-response-dto.ts | 28 +++++ .../models/member-profile-response-dto.ts | 4 +- .../my-group-study-application-response.ts | 4 +- ... => my-group-study-apply-list-item-dto.ts} | 14 +-- src/api/openapi/models/oauth2-user-info.ts | 22 ++++ ...age-response-group-study-list-item-dto.ts} | 6 +- ...nse-my-group-study-apply-list-item-dto.ts} | 6 +- .../models/participating-study-info.ts | 4 +- ...{resized-image.ts => resized-image-dto.ts} | 2 +- ...ber.ts => study-reservation-member-dto.ts} | 6 +- .../study-reservation-response-content.ts | 4 +- src/api/openapi/models/temporal-token.ts | 20 ++++ .../models/thread-comment-response-content.ts | 4 +- .../models/thread-summary-response-content.ts | 4 +- 119 files changed, 1596 insertions(+), 345 deletions(-) create mode 100644 src/api/openapi/docs/BaseResponseGrantedTokenInfo.md create mode 100644 src/api/openapi/docs/BaseResponseGroupStudyMembersResponseContent.md create mode 100644 src/api/openapi/docs/BaseResponseGroupStudyProgressGradesResponse.md create mode 100644 src/api/openapi/docs/BaseResponseHomeworkSubmissionResponse.md create mode 100644 src/api/openapi/docs/BaseResponseMatchingRequestResponse.md create mode 100644 src/api/openapi/docs/BaseResponseMemberAccountHistoryResponseDto.md create mode 100644 src/api/openapi/docs/BaseResponseObject.md create mode 100644 src/api/openapi/docs/BaseResponseString.md create mode 100644 src/api/openapi/docs/BaseResponseStudySpaceIsParticipateResponse.md create mode 100644 src/api/openapi/docs/BaseResponseTemporalToken.md create mode 100644 src/api/openapi/docs/BaseResponseVoid.md create mode 100644 src/api/openapi/docs/ChangeHistDto.md rename src/api/openapi/docs/{CursorResponseDtoStudyReservationMember.md => CursorResponseDtoStudyReservationMemberDto.md} (61%) create mode 100644 src/api/openapi/docs/GrantedTokenInfo.md rename src/api/openapi/docs/{GroupStudyApplyResponseContent.md => GroupStudyApplyResponseDto.md} (91%) rename src/api/openapi/docs/{GroupStudyBasicInfoResponse.md => GroupStudyBasicInfoResponseDto.md} (89%) rename src/api/openapi/docs/{GroupStudyCreationResponseContent.md => GroupStudyCreationResponseDto.md} (82%) rename src/api/openapi/docs/{GroupStudyDetailInfoResponse.md => GroupStudyDetailInfoResponseDto.md} (79%) delete mode 100644 src/api/openapi/docs/GroupStudyDetailResponseContent.md create mode 100644 src/api/openapi/docs/GroupStudyFullResponseDto.md rename src/api/openapi/docs/{GroupStudInterviewPostResponseContent.md => GroupStudyInterviewPostResponseDto.md} (83%) delete mode 100644 src/api/openapi/docs/GroupStudyListItem.md create mode 100644 src/api/openapi/docs/GroupStudyListItemDto.md create mode 100644 src/api/openapi/docs/GroupStudyProgressGradeResponse.md create mode 100644 src/api/openapi/docs/GroupStudyProgressGradesResponse.md rename src/api/openapi/docs/{GroupStudySimpleInfoResponse.md => GroupStudySimpleInfoResponseDto.md} (69%) create mode 100644 src/api/openapi/docs/HomeworkSubmissionResponse.md rename src/api/openapi/docs/{Image.md => ImageDto.md} (60%) create mode 100644 src/api/openapi/docs/MatchingRequestResponse.md create mode 100644 src/api/openapi/docs/MemberAccountHistoryResponseDto.md delete mode 100644 src/api/openapi/docs/MyGroupStudyApplyListItem.md create mode 100644 src/api/openapi/docs/MyGroupStudyApplyListItemDto.md create mode 100644 src/api/openapi/docs/OAuth2UserInfo.md rename src/api/openapi/docs/{PageResponseGroupStudyListItem.md => PageResponseGroupStudyListItemDto.md} (74%) rename src/api/openapi/docs/{PageResponseMyGroupStudyApplyListItem.md => PageResponseMyGroupStudyApplyListItemDto.md} (72%) rename src/api/openapi/docs/{ResizedImage.md => ResizedImageDto.md} (87%) rename src/api/openapi/docs/{StudyReservationMember.md => StudyReservationMemberDto.md} (73%) create mode 100644 src/api/openapi/docs/TemporalToken.md create mode 100644 src/api/openapi/models/base-response-granted-token-info.ts create mode 100644 src/api/openapi/models/base-response-group-study-members-response-content.ts create mode 100644 src/api/openapi/models/base-response-group-study-progress-grades-response.ts create mode 100644 src/api/openapi/models/base-response-homework-submission-response.ts create mode 100644 src/api/openapi/models/base-response-matching-request-response.ts create mode 100644 src/api/openapi/models/base-response-member-account-history-response-dto.ts create mode 100644 src/api/openapi/models/base-response-object.ts create mode 100644 src/api/openapi/models/base-response-string.ts create mode 100644 src/api/openapi/models/base-response-study-space-is-participate-response.ts create mode 100644 src/api/openapi/models/base-response-temporal-token.ts create mode 100644 src/api/openapi/models/base-response-void.ts create mode 100644 src/api/openapi/models/change-hist-dto.ts rename src/api/openapi/models/{cursor-response-dto-study-reservation-member.ts => cursor-response-dto-study-reservation-member-dto.ts} (67%) create mode 100644 src/api/openapi/models/granted-token-info.ts rename src/api/openapi/models/{group-study-apply-response-content.ts => group-study-apply-response-dto.ts} (70%) rename src/api/openapi/models/{group-study-basic-info-response.ts => group-study-basic-info-response-dto.ts} (51%) rename src/api/openapi/models/{group-study-creation-response-content.ts => group-study-creation-response-dto.ts} (87%) rename src/api/openapi/models/{group-study-detail-info-response.ts => group-study-detail-info-response-dto.ts} (85%) rename src/api/openapi/models/{group-study-detail-response-content.ts => group-study-full-response-dto.ts} (53%) rename src/api/openapi/models/{group-stud-interview-post-response-content.ts => group-study-interview-post-response-dto.ts} (93%) rename src/api/openapi/models/{group-study-list-item.ts => group-study-list-item-dto.ts} (63%) create mode 100644 src/api/openapi/models/group-study-progress-grade-response.ts create mode 100644 src/api/openapi/models/group-study-progress-grades-response.ts rename src/api/openapi/models/{group-study-simple-info-response.ts => group-study-simple-info-response-dto.ts} (83%) create mode 100644 src/api/openapi/models/homework-submission-response.ts rename src/api/openapi/models/{image.ts => image-dto.ts} (79%) create mode 100644 src/api/openapi/models/matching-request-response.ts create mode 100644 src/api/openapi/models/member-account-history-response-dto.ts rename src/api/openapi/models/{my-group-study-apply-list-item.ts => my-group-study-apply-list-item-dto.ts} (62%) create mode 100644 src/api/openapi/models/oauth2-user-info.ts rename src/api/openapi/models/{page-response-group-study-list-item.ts => page-response-group-study-list-item-dto.ts} (74%) rename src/api/openapi/models/{page-response-my-group-study-apply-list-item.ts => page-response-my-group-study-apply-list-item-dto.ts} (71%) rename src/api/openapi/models/{resized-image.ts => resized-image-dto.ts} (94%) rename src/api/openapi/models/{study-reservation-member.ts => study-reservation-member-dto.ts} (78%) create mode 100644 src/api/openapi/models/temporal-token.ts diff --git a/src/api/openapi/.openapi-generator/FILES b/src/api/openapi/.openapi-generator/FILES index 0d7fe5c4..d461ca76 100644 --- a/src/api/openapi/.openapi-generator/FILES +++ b/src/api/openapi/.openapi-generator/FILES @@ -61,15 +61,27 @@ docs/BankListResponseSchema.md docs/BankResponse.md docs/BankSearchApi.md docs/BaseResponse.md +docs/BaseResponseGrantedTokenInfo.md +docs/BaseResponseGroupStudyMembersResponseContent.md +docs/BaseResponseGroupStudyProgressGradesResponse.md +docs/BaseResponseHomeworkSubmissionResponse.md +docs/BaseResponseMatchingRequestResponse.md +docs/BaseResponseMemberAccountHistoryResponseDto.md +docs/BaseResponseObject.md +docs/BaseResponseString.md +docs/BaseResponseStudySpaceIsParticipateResponse.md +docs/BaseResponseTemporalToken.md +docs/BaseResponseVoid.md docs/CalendarDayStatus.md docs/CareerListResponse.md docs/CareerResponseDto.md +docs/ChangeHistDto.md docs/CompleteDailyStudyRequest.md docs/CompleteDailyStudyRequestSchema.md docs/CreateMissionResponseSchema.md docs/CreateStudySpaceRequest.md docs/CursorResponseDtoDailyStudyResponse.md -docs/CursorResponseDtoStudyReservationMember.md +docs/CursorResponseDtoStudyReservationMemberDto.md docs/DailyStudyApi.md docs/DailyStudyResponse.md docs/DocumentedErrorCodeControllerApi.md @@ -88,7 +100,7 @@ docs/GetStudyDashboardResponseSchema.md docs/GetStudySpaceResponseSchema.md docs/GetStudySpacesResponseSchema.md docs/GetTodayMyDailyStudyResponseSchema.md -docs/GroupStudInterviewPostResponseContent.md +docs/GrantedTokenInfo.md docs/GroupStudyApplicant.md docs/GroupStudyApplyApi.md docs/GroupStudyApplyCreationResponse.md @@ -100,23 +112,24 @@ docs/GroupStudyApplyProcessRequest.md docs/GroupStudyApplyProcessResponse.md docs/GroupStudyApplyProcessResponseContent.md docs/GroupStudyApplyRequest.md -docs/GroupStudyApplyResponseContent.md +docs/GroupStudyApplyResponseDto.md docs/GroupStudyApplyUpdateRequest.md docs/GroupStudyApplyUpdateResponseContent.md docs/GroupStudyBasicInfoRequestDto.md -docs/GroupStudyBasicInfoResponse.md +docs/GroupStudyBasicInfoResponseDto.md docs/GroupStudyBasicInfoUpdateRequestDto.md docs/GroupStudyCreationRequestDto.md docs/GroupStudyCreationResponse.md -docs/GroupStudyCreationResponseContent.md +docs/GroupStudyCreationResponseDto.md docs/GroupStudyDetailInfoRequestDto.md -docs/GroupStudyDetailInfoResponse.md +docs/GroupStudyDetailInfoResponseDto.md docs/GroupStudyDetailResponse.md -docs/GroupStudyDetailResponseContent.md +docs/GroupStudyFullResponseDto.md docs/GroupStudyHomeworkApi.md docs/GroupStudyInterviewPostRequest.md +docs/GroupStudyInterviewPostResponseDto.md docs/GroupStudyInterviewPostUpdateResponse.md -docs/GroupStudyListItem.md +docs/GroupStudyListItemDto.md docs/GroupStudyListResponse.md docs/GroupStudyManagementApi.md docs/GroupStudyMemberApi.md @@ -130,7 +143,9 @@ docs/GroupStudyNoticeApi.md docs/GroupStudyNoticeRequest.md docs/GroupStudyNoticeResponse.md docs/GroupStudyNoticeResponseContent.md -docs/GroupStudySimpleInfoResponse.md +docs/GroupStudyProgressGradeResponse.md +docs/GroupStudyProgressGradesResponse.md +docs/GroupStudySimpleInfoResponseDto.md docs/GroupStudyThreadApi.md docs/GroupStudyThreadCommentRequest.md docs/GroupStudyThreadRequest.md @@ -141,8 +156,9 @@ docs/HasMemberNewNotificationResponse.md docs/HasMemberNewNotificationResponseSchema.md docs/HomeworkEditRequest.md docs/HomeworkSubmissionRequest.md +docs/HomeworkSubmissionResponse.md docs/IdNameDto.md -docs/Image.md +docs/ImageDto.md docs/ImageSizeType.md docs/InterviewAnswer.md docs/InterviewQuestion.md @@ -151,8 +167,10 @@ docs/JobResponseDto.md docs/LocalTime.md docs/MatchingApi.md docs/MatchingAttributes.md +docs/MatchingRequestResponse.md docs/MatchingSystemStatusResponse.md docs/MatchingSystemStatusSchema.md +docs/MemberAccountHistoryResponseDto.md docs/MemberApi.md docs/MemberCreationRequest.md docs/MemberCreationResponse.md @@ -179,7 +197,7 @@ docs/MissionTaskDto.md docs/MissionUpdateRequest.md docs/MonthlyCalendarResponse.md docs/MyGroupStudyApplicationResponse.md -docs/MyGroupStudyApplyListItem.md +docs/MyGroupStudyApplyListItemDto.md docs/NicknameAvailabilityResponse.md docs/NicknameAvailabilityResponseDto.md docs/NoContentResponse.md @@ -188,14 +206,15 @@ docs/NotificationCategoriesResponse.md docs/NotificationCategoriesResponseSchema.md docs/NotificationTopicResponse.md docs/NumberResponse.md +docs/OAuth2UserInfo.md docs/OptionalHomeworkSubmission.md docs/PageAdminTransactionListResponseSchema.md docs/PageResponseAdminTransactionListResponse.md docs/PageResponseGroupStudyApplyListItem.md -docs/PageResponseGroupStudyListItem.md +docs/PageResponseGroupStudyListItemDto.md docs/PageResponseMemberNotificationResponse.md docs/PageResponseMissionListResponse.md -docs/PageResponseMyGroupStudyApplyListItem.md +docs/PageResponseMyGroupStudyApplyListItemDto.md docs/PageResponseParticipatingStudyInfo.md docs/PageResponseStudyRefundSummaryResponse.md docs/PageResponseStudySettlementSummaryResponse.md @@ -230,7 +249,7 @@ docs/RequestAutoStudyMatchingDto.md docs/ResetWeeklyMatchingRequest.md docs/ResetWeeklyMatchingResponse.md docs/ResetWeeklyMatchingSchema.md -docs/ResizedImage.md +docs/ResizedImageDto.md docs/SettlementAccountApi.md docs/SettlementAccountRegisterRequest.md docs/SettlementAccountResponse.md @@ -256,7 +275,7 @@ docs/StudyRefundDetailResponse.md docs/StudyRefundDetailResponseSchema.md docs/StudyRefundRejectRequest.md docs/StudyRefundSummaryResponse.md -docs/StudyReservationMember.md +docs/StudyReservationMemberDto.md docs/StudyReservationResponse.md docs/StudyReservationResponseContent.md docs/StudyReviewApi.md @@ -271,6 +290,7 @@ docs/StudySubjectDto.md docs/StudySubjectListResponse.md docs/TechStackApi.md docs/TechStackResponse.md +docs/TemporalToken.md docs/ThreadCommentResponseContent.md docs/ThreadCommentResponseSchema.md docs/ThreadSummaryResponse.md @@ -301,16 +321,28 @@ models/available-study-time-dto.ts models/available-study-time-list-response.ts models/bank-list-response-schema.ts models/bank-response.ts +models/base-response-granted-token-info.ts +models/base-response-group-study-members-response-content.ts +models/base-response-group-study-progress-grades-response.ts +models/base-response-homework-submission-response.ts +models/base-response-matching-request-response.ts +models/base-response-member-account-history-response-dto.ts +models/base-response-object.ts +models/base-response-string.ts +models/base-response-study-space-is-participate-response.ts +models/base-response-temporal-token.ts +models/base-response-void.ts models/base-response.ts models/calendar-day-status.ts models/career-list-response.ts models/career-response-dto.ts +models/change-hist-dto.ts models/complete-daily-study-request-schema.ts models/complete-daily-study-request.ts models/create-mission-response-schema.ts models/create-study-space-request.ts models/cursor-response-dto-daily-study-response.ts -models/cursor-response-dto-study-reservation-member.ts +models/cursor-response-dto-study-reservation-member-dto.ts models/daily-study-response.ts models/error-response.ts models/evaluation-request.ts @@ -325,7 +357,7 @@ models/get-study-dashboard-response-schema.ts models/get-study-space-response-schema.ts models/get-study-spaces-response-schema.ts models/get-today-my-daily-study-response-schema.ts -models/group-stud-interview-post-response-content.ts +models/granted-token-info.ts models/group-study-applicant.ts models/group-study-apply-creation-response-content.ts models/group-study-apply-creation-response.ts @@ -336,22 +368,23 @@ models/group-study-apply-process-request.ts models/group-study-apply-process-response-content.ts models/group-study-apply-process-response.ts models/group-study-apply-request.ts -models/group-study-apply-response-content.ts +models/group-study-apply-response-dto.ts models/group-study-apply-update-request.ts models/group-study-apply-update-response-content.ts models/group-study-basic-info-request-dto.ts -models/group-study-basic-info-response.ts +models/group-study-basic-info-response-dto.ts models/group-study-basic-info-update-request-dto.ts models/group-study-creation-request-dto.ts -models/group-study-creation-response-content.ts +models/group-study-creation-response-dto.ts models/group-study-creation-response.ts models/group-study-detail-info-request-dto.ts -models/group-study-detail-info-response.ts -models/group-study-detail-response-content.ts +models/group-study-detail-info-response-dto.ts models/group-study-detail-response.ts +models/group-study-full-response-dto.ts models/group-study-interview-post-request.ts +models/group-study-interview-post-response-dto.ts models/group-study-interview-post-update-response.ts -models/group-study-list-item.ts +models/group-study-list-item-dto.ts models/group-study-list-response.ts models/group-study-member-progress-grade-response.ts models/group-study-member-progress-history-response.ts @@ -362,7 +395,9 @@ models/group-study-members-response.ts models/group-study-notice-request.ts models/group-study-notice-response-content.ts models/group-study-notice-response.ts -models/group-study-simple-info-response.ts +models/group-study-progress-grade-response.ts +models/group-study-progress-grades-response.ts +models/group-study-simple-info-response-dto.ts models/group-study-thread-comment-request.ts models/group-study-thread-request.ts models/group-study-update-request.ts @@ -372,9 +407,10 @@ models/has-member-new-notification-response-schema.ts models/has-member-new-notification-response.ts models/homework-edit-request.ts models/homework-submission-request.ts +models/homework-submission-response.ts models/id-name-dto.ts +models/image-dto.ts models/image-size-type.ts -models/image.ts models/index.ts models/interview-answer.ts models/interview-question.ts @@ -382,8 +418,10 @@ models/job-list-response.ts models/job-response-dto.ts models/local-time.ts models/matching-attributes.ts +models/matching-request-response.ts models/matching-system-status-response.ts models/matching-system-status-schema.ts +models/member-account-history-response-dto.ts models/member-creation-request.ts models/member-creation-response-content.ts models/member-creation-response.ts @@ -406,7 +444,7 @@ models/mission-task-dto.ts models/mission-update-request.ts models/monthly-calendar-response.ts models/my-group-study-application-response.ts -models/my-group-study-apply-list-item.ts +models/my-group-study-apply-list-item-dto.ts models/nickname-availability-response-dto.ts models/nickname-availability-response.ts models/no-content-response.ts @@ -414,14 +452,15 @@ models/notification-categories-response-schema.ts models/notification-categories-response.ts models/notification-topic-response.ts models/number-response.ts +models/oauth2-user-info.ts models/optional-homework-submission.ts models/page-admin-transaction-list-response-schema.ts models/page-response-admin-transaction-list-response.ts models/page-response-group-study-apply-list-item.ts -models/page-response-group-study-list-item.ts +models/page-response-group-study-list-item-dto.ts models/page-response-member-notification-response.ts models/page-response-mission-list-response.ts -models/page-response-my-group-study-apply-list-item.ts +models/page-response-my-group-study-apply-list-item-dto.ts models/page-response-participating-study-info.ts models/page-response-study-refund-summary-response.ts models/page-response-study-settlement-summary-response.ts @@ -453,7 +492,7 @@ models/request-auto-study-matching-dto.ts models/reset-weekly-matching-request.ts models/reset-weekly-matching-response.ts models/reset-weekly-matching-schema.ts -models/resized-image.ts +models/resized-image-dto.ts models/settlement-account-register-request.ts models/settlement-account-response-schema.ts models/settlement-account-response.ts @@ -476,7 +515,7 @@ models/study-refund-detail-response-schema.ts models/study-refund-detail-response.ts models/study-refund-reject-request.ts models/study-refund-summary-response.ts -models/study-reservation-member.ts +models/study-reservation-member-dto.ts models/study-reservation-response-content.ts models/study-reservation-response.ts models/study-settlement-create-request.ts @@ -488,6 +527,7 @@ models/study-space-is-participate-response.ts models/study-subject-dto.ts models/study-subject-list-response.ts models/tech-stack-response.ts +models/temporal-token.ts models/thread-comment-response-content.ts models/thread-comment-response-schema.ts models/thread-summary-response-content.ts diff --git a/src/api/openapi/api/admin-api.ts b/src/api/openapi/api/admin-api.ts index 68857bd0..84f866da 100644 --- a/src/api/openapi/api/admin-api.ts +++ b/src/api/openapi/api/admin-api.ts @@ -22,7 +22,7 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseMemberAccountHistoryResponseDto } from '../models'; // @ts-ignore import type { ErrorResponse } from '../models'; // @ts-ignore @@ -336,7 +336,7 @@ export const AdminApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getMemberAccountHistoryResponse(memberId: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async getMemberAccountHistoryResponse(memberId: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getMemberAccountHistoryResponse(memberId, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['AdminApi.getMemberAccountHistoryResponse']?.[localVarOperationServerIndex]?.url; @@ -427,7 +427,7 @@ export const AdminApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMemberAccountHistoryResponse(memberId: number, options?: RawAxiosRequestConfig): AxiosPromise { + getMemberAccountHistoryResponse(memberId: number, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.getMemberAccountHistoryResponse(memberId, options).then((request) => request(axios, basePath)); }, /** diff --git a/src/api/openapi/api/admin-matching-api.ts b/src/api/openapi/api/admin-matching-api.ts index 2650fb3c..97fb259a 100644 --- a/src/api/openapi/api/admin-matching-api.ts +++ b/src/api/openapi/api/admin-matching-api.ts @@ -30,6 +30,8 @@ import type { AutoRunMatchingRequestDto } from '../models'; // @ts-ignore import type { BaseResponse } from '../models'; // @ts-ignore +import type { BaseResponseMatchingRequestResponse } from '../models'; +// @ts-ignore import type { ErrorResponse } from '../models'; // @ts-ignore import type { ResetWeeklyMatchingRequest } from '../models'; @@ -330,7 +332,7 @@ export const AdminMatchingApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async createMatchingRequestByAdmin(adminMatchingCreateRequest: AdminMatchingCreateRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async createMatchingRequestByAdmin(adminMatchingCreateRequest: AdminMatchingCreateRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.createMatchingRequestByAdmin(adminMatchingCreateRequest, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['AdminMatchingApi.createMatchingRequestByAdmin']?.[localVarOperationServerIndex]?.url; @@ -368,7 +370,7 @@ export const AdminMatchingApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getMatchingRequest(matchingRequestId: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async getMatchingRequest(matchingRequestId: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.getMatchingRequest(matchingRequestId, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['AdminMatchingApi.getMatchingRequest']?.[localVarOperationServerIndex]?.url; @@ -408,7 +410,7 @@ export const AdminMatchingApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async updateMatchingRequestByAdmin(matchingRequestId: number, adminMatchingUpdateRequest: AdminMatchingUpdateRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async updateMatchingRequestByAdmin(matchingRequestId: number, adminMatchingUpdateRequest: AdminMatchingUpdateRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.updateMatchingRequestByAdmin(matchingRequestId, adminMatchingUpdateRequest, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['AdminMatchingApi.updateMatchingRequestByAdmin']?.[localVarOperationServerIndex]?.url; @@ -430,7 +432,7 @@ export const AdminMatchingApiFactory = function (configuration?: Configuration, * @param {*} [options] Override http request option. * @throws {RequiredError} */ - createMatchingRequestByAdmin(adminMatchingCreateRequest: AdminMatchingCreateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + createMatchingRequestByAdmin(adminMatchingCreateRequest: AdminMatchingCreateRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.createMatchingRequestByAdmin(adminMatchingCreateRequest, options).then((request) => request(axios, basePath)); }, /** @@ -459,7 +461,7 @@ export const AdminMatchingApiFactory = function (configuration?: Configuration, * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMatchingRequest(matchingRequestId: number, options?: RawAxiosRequestConfig): AxiosPromise { + getMatchingRequest(matchingRequestId: number, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.getMatchingRequest(matchingRequestId, options).then((request) => request(axios, basePath)); }, /** @@ -490,7 +492,7 @@ export const AdminMatchingApiFactory = function (configuration?: Configuration, * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateMatchingRequestByAdmin(matchingRequestId: number, adminMatchingUpdateRequest: AdminMatchingUpdateRequest, options?: RawAxiosRequestConfig): AxiosPromise { + updateMatchingRequestByAdmin(matchingRequestId: number, adminMatchingUpdateRequest: AdminMatchingUpdateRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.updateMatchingRequestByAdmin(matchingRequestId, adminMatchingUpdateRequest, options).then((request) => request(axios, basePath)); }, }; diff --git a/src/api/openapi/api/auth-api.ts b/src/api/openapi/api/auth-api.ts index 8d33d963..c089fcb1 100644 --- a/src/api/openapi/api/auth-api.ts +++ b/src/api/openapi/api/auth-api.ts @@ -22,7 +22,9 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseGrantedTokenInfo } from '../models'; +// @ts-ignore +import type { BaseResponseVoid } from '../models'; // @ts-ignore import type { NumberResponse } from '../models'; // @ts-ignore @@ -220,7 +222,7 @@ export const AuthApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async logout(referer?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async logout(referer?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.logout(referer, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['AuthApi.logout']?.[localVarOperationServerIndex]?.url; @@ -279,7 +281,7 @@ export const AuthApiFactory = function (configuration?: Configuration, basePath? * @param {*} [options] Override http request option. * @throws {RequiredError} */ - logout(referer?: string, options?: RawAxiosRequestConfig): AxiosPromise { + logout(referer?: string, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.logout(referer, options).then((request) => request(axios, basePath)); }, /** diff --git a/src/api/openapi/api/daily-study-api.ts b/src/api/openapi/api/daily-study-api.ts index fb714b46..2c51b334 100644 --- a/src/api/openapi/api/daily-study-api.ts +++ b/src/api/openapi/api/daily-study-api.ts @@ -24,6 +24,8 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError // @ts-ignore import type { BaseResponse } from '../models'; // @ts-ignore +import type { BaseResponseString } from '../models'; +// @ts-ignore import type { CompleteDailyStudyRequestSchema } from '../models'; // @ts-ignore import type { GetDailyStudiesResponseSchema } from '../models'; diff --git a/src/api/openapi/api/file-controller-api.ts b/src/api/openapi/api/file-controller-api.ts index 42c7c454..6d9a527a 100644 --- a/src/api/openapi/api/file-controller-api.ts +++ b/src/api/openapi/api/file-controller-api.ts @@ -22,7 +22,7 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseVoid } from '../models'; // @ts-ignore import type { UploadProfileImageRequest } from '../models'; /** @@ -95,7 +95,7 @@ export const FileControllerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async uploadProfileImage(filePath: string, fileClassification: string, uploadProfileImageRequest?: UploadProfileImageRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async uploadProfileImage(filePath: string, fileClassification: string, uploadProfileImageRequest?: UploadProfileImageRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.uploadProfileImage(filePath, fileClassification, uploadProfileImageRequest, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['FileControllerApi.uploadProfileImage']?.[localVarOperationServerIndex]?.url; @@ -118,7 +118,7 @@ export const FileControllerApiFactory = function (configuration?: Configuration, * @param {*} [options] Override http request option. * @throws {RequiredError} */ - uploadProfileImage(filePath: string, fileClassification: string, uploadProfileImageRequest?: UploadProfileImageRequest, options?: RawAxiosRequestConfig): AxiosPromise { + uploadProfileImage(filePath: string, fileClassification: string, uploadProfileImageRequest?: UploadProfileImageRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.uploadProfileImage(filePath, fileClassification, uploadProfileImageRequest, options).then((request) => request(axios, basePath)); }, }; diff --git a/src/api/openapi/api/group-study-homework-api.ts b/src/api/openapi/api/group-study-homework-api.ts index a478062a..e09207a4 100644 --- a/src/api/openapi/api/group-study-homework-api.ts +++ b/src/api/openapi/api/group-study-homework-api.ts @@ -22,7 +22,9 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseHomeworkSubmissionResponse } from '../models'; +// @ts-ignore +import type { BaseResponseVoid } from '../models'; // @ts-ignore import type { HomeworkEditRequest } from '../models'; // @ts-ignore @@ -134,7 +136,7 @@ export const GroupStudyHomeworkApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async editHomework(homeworkId: number, homeworkEditRequest: HomeworkEditRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async editHomework(homeworkId: number, homeworkEditRequest: HomeworkEditRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.editHomework(homeworkId, homeworkEditRequest, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['GroupStudyHomeworkApi.editHomework']?.[localVarOperationServerIndex]?.url; @@ -147,7 +149,7 @@ export const GroupStudyHomeworkApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async submitHomework(missionId: number, homeworkSubmissionRequest: HomeworkSubmissionRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async submitHomework(missionId: number, homeworkSubmissionRequest: HomeworkSubmissionRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.submitHomework(missionId, homeworkSubmissionRequest, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['GroupStudyHomeworkApi.submitHomework']?.[localVarOperationServerIndex]?.url; @@ -169,7 +171,7 @@ export const GroupStudyHomeworkApiFactory = function (configuration?: Configurat * @param {*} [options] Override http request option. * @throws {RequiredError} */ - editHomework(homeworkId: number, homeworkEditRequest: HomeworkEditRequest, options?: RawAxiosRequestConfig): AxiosPromise { + editHomework(homeworkId: number, homeworkEditRequest: HomeworkEditRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.editHomework(homeworkId, homeworkEditRequest, options).then((request) => request(axios, basePath)); }, /** @@ -179,7 +181,7 @@ export const GroupStudyHomeworkApiFactory = function (configuration?: Configurat * @param {*} [options] Override http request option. * @throws {RequiredError} */ - submitHomework(missionId: number, homeworkSubmissionRequest: HomeworkSubmissionRequest, options?: RawAxiosRequestConfig): AxiosPromise { + submitHomework(missionId: number, homeworkSubmissionRequest: HomeworkSubmissionRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.submitHomework(missionId, homeworkSubmissionRequest, options).then((request) => request(axios, basePath)); }, }; diff --git a/src/api/openapi/api/group-study-member-api.ts b/src/api/openapi/api/group-study-member-api.ts index 7922711b..3ed5b8fe 100644 --- a/src/api/openapi/api/group-study-member-api.ts +++ b/src/api/openapi/api/group-study-member-api.ts @@ -22,7 +22,11 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseGroupStudyMembersResponseContent } from '../models'; +// @ts-ignore +import type { BaseResponseGroupStudyProgressGradesResponse } from '../models'; +// @ts-ignore +import type { BaseResponseString } from '../models'; // @ts-ignore import type { ErrorResponse } from '../models'; // @ts-ignore diff --git a/src/api/openapi/api/matching-api.ts b/src/api/openapi/api/matching-api.ts index d4e33ba8..55b2b867 100644 --- a/src/api/openapi/api/matching-api.ts +++ b/src/api/openapi/api/matching-api.ts @@ -24,6 +24,8 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError // @ts-ignore import type { BaseResponse } from '../models'; // @ts-ignore +import type { BaseResponseObject } from '../models'; +// @ts-ignore import type { ErrorResponse } from '../models'; // @ts-ignore import type { MatchingSystemStatusSchema } from '../models'; diff --git a/src/api/openapi/api/member-tmp-controller-api.ts b/src/api/openapi/api/member-tmp-controller-api.ts index f1c400e2..edb526c9 100644 --- a/src/api/openapi/api/member-tmp-controller-api.ts +++ b/src/api/openapi/api/member-tmp-controller-api.ts @@ -22,7 +22,7 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseVoid } from '../models'; /** * MemberTmpControllerApi - axios parameter creator */ @@ -80,7 +80,7 @@ export const MemberTmpControllerApiFp = function(configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async deleteMemberPermanently(memberId: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async deleteMemberPermanently(memberId: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.deleteMemberPermanently(memberId, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['MemberTmpControllerApi.deleteMemberPermanently']?.[localVarOperationServerIndex]?.url; @@ -101,7 +101,7 @@ export const MemberTmpControllerApiFactory = function (configuration?: Configura * @param {*} [options] Override http request option. * @throws {RequiredError} */ - deleteMemberPermanently(memberId: number, options?: RawAxiosRequestConfig): AxiosPromise { + deleteMemberPermanently(memberId: number, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.deleteMemberPermanently(memberId, options).then((request) => request(axios, basePath)); }, }; diff --git a/src/api/openapi/api/settlement-account-api.ts b/src/api/openapi/api/settlement-account-api.ts index bfa0913d..3bba8644 100644 --- a/src/api/openapi/api/settlement-account-api.ts +++ b/src/api/openapi/api/settlement-account-api.ts @@ -22,7 +22,7 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseVoid } from '../models'; // @ts-ignore import type { SettlementAccountRegisterRequest } from '../models'; // @ts-ignore @@ -195,7 +195,7 @@ export const SettlementAccountApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async _delete(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async _delete(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator._delete(options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['SettlementAccountApi._delete']?.[localVarOperationServerIndex]?.url; @@ -254,7 +254,7 @@ export const SettlementAccountApiFactory = function (configuration?: Configurati * @param {*} [options] Override http request option. * @throws {RequiredError} */ - _delete(options?: RawAxiosRequestConfig): AxiosPromise { + _delete(options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp._delete(options).then((request) => request(axios, basePath)); }, /** diff --git a/src/api/openapi/api/study-space-api.ts b/src/api/openapi/api/study-space-api.ts index 12df94c2..433cab81 100644 --- a/src/api/openapi/api/study-space-api.ts +++ b/src/api/openapi/api/study-space-api.ts @@ -22,7 +22,9 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseString } from '../models'; +// @ts-ignore +import type { BaseResponseStudySpaceIsParticipateResponse } from '../models'; // @ts-ignore import type { CreateStudySpaceRequest } from '../models'; // @ts-ignore diff --git a/src/api/openapi/api/token-apiapi.ts b/src/api/openapi/api/token-apiapi.ts index 15cd9281..85eeed6c 100644 --- a/src/api/openapi/api/token-apiapi.ts +++ b/src/api/openapi/api/token-apiapi.ts @@ -22,7 +22,7 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; // @ts-ignore -import type { BaseResponse } from '../models'; +import type { BaseResponseTemporalToken } from '../models'; /** * TokenAPIApi - axios parameter creator */ @@ -91,7 +91,7 @@ export const TokenAPIApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async generateToken(memberId: number, roleId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async generateToken(memberId: number, roleId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.generateToken(memberId, roleId, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['TokenAPIApi.generateToken']?.[localVarOperationServerIndex]?.url; @@ -113,7 +113,7 @@ export const TokenAPIApiFactory = function (configuration?: Configuration, baseP * @param {*} [options] Override http request option. * @throws {RequiredError} */ - generateToken(memberId: number, roleId: string, options?: RawAxiosRequestConfig): AxiosPromise { + generateToken(memberId: number, roleId: string, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.generateToken(memberId, roleId, options).then((request) => request(axios, basePath)); }, }; diff --git a/src/api/openapi/docs/AdminApi.md b/src/api/openapi/docs/AdminApi.md index e9e863af..329dec7d 100644 --- a/src/api/openapi/docs/AdminApi.md +++ b/src/api/openapi/docs/AdminApi.md @@ -77,7 +77,7 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getMemberAccountHistoryResponse** -> BaseResponse getMemberAccountHistoryResponse() +> BaseResponseMemberAccountHistoryResponseDto getMemberAccountHistoryResponse() ### Example @@ -107,7 +107,7 @@ const { status, data } = await apiInstance.getMemberAccountHistoryResponse( ### Return type -**BaseResponse** +**BaseResponseMemberAccountHistoryResponseDto** ### Authorization diff --git a/src/api/openapi/docs/AdminMatchingApi.md b/src/api/openapi/docs/AdminMatchingApi.md index 822fc674..04894065 100644 --- a/src/api/openapi/docs/AdminMatchingApi.md +++ b/src/api/openapi/docs/AdminMatchingApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://test-api.zeroone.it.kr* |[**updateMatchingRequestByAdmin**](#updatematchingrequestbyadmin) | **PATCH** /api/v1/admin/matching/requests/{matchingRequestId} | 관리자 매칭 변경/취소| # **createMatchingRequestByAdmin** -> BaseResponse createMatchingRequestByAdmin(adminMatchingCreateRequest) +> BaseResponseMatchingRequestResponse createMatchingRequestByAdmin(adminMatchingCreateRequest) 관리자가 수동으로 매칭 요청을 생성합니다. weeklyPeriodIdentifier는 매칭이 속할 주간을 지정하며, 해당 주간의 토요일 날짜를 사용합니다. @@ -45,7 +45,7 @@ const { status, data } = await apiInstance.createMatchingRequestByAdmin( ### Return type -**BaseResponse** +**BaseResponseMatchingRequestResponse** ### Authorization @@ -166,7 +166,7 @@ This endpoint does not have any parameters. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getMatchingRequest** -> BaseResponse getMatchingRequest() +> BaseResponseMatchingRequestResponse getMatchingRequest() 관리자가 특정 매칭 요청을 조회합니다. @@ -197,7 +197,7 @@ const { status, data } = await apiInstance.getMatchingRequest( ### Return type -**BaseResponse** +**BaseResponseMatchingRequestResponse** ### Authorization @@ -332,7 +332,7 @@ const { status, data } = await apiInstance.runAutoMatchingJob( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **updateMatchingRequestByAdmin** -> BaseResponse updateMatchingRequestByAdmin(adminMatchingUpdateRequest) +> BaseResponseMatchingRequestResponse updateMatchingRequestByAdmin(adminMatchingUpdateRequest) 관리자가 특정 매칭 요청의 상태, 파트너 등을 변경합니다. @@ -367,7 +367,7 @@ const { status, data } = await apiInstance.updateMatchingRequestByAdmin( ### Return type -**BaseResponse** +**BaseResponseMatchingRequestResponse** ### Authorization diff --git a/src/api/openapi/docs/AuthApi.md b/src/api/openapi/docs/AuthApi.md index f3cc0acc..b89a114a 100644 --- a/src/api/openapi/docs/AuthApi.md +++ b/src/api/openapi/docs/AuthApi.md @@ -62,7 +62,7 @@ const { status, data } = await apiInstance.accessToken( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **logout** -> BaseResponse logout() +> BaseResponseVoid logout() Cookie에 저장된 Refresh token을 제거함으로써 로그아웃 진행. 프론트에서 Access token을 제거할 필요가 있음 @@ -93,7 +93,7 @@ const { status, data } = await apiInstance.logout( ### Return type -**BaseResponse** +**BaseResponseVoid** ### Authorization diff --git a/src/api/openapi/docs/BaseResponseGrantedTokenInfo.md b/src/api/openapi/docs/BaseResponseGrantedTokenInfo.md new file mode 100644 index 00000000..9e9af84e --- /dev/null +++ b/src/api/openapi/docs/BaseResponseGrantedTokenInfo.md @@ -0,0 +1,26 @@ +# BaseResponseGrantedTokenInfo + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**GrantedTokenInfo**](GrantedTokenInfo.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseGrantedTokenInfo } from './api'; + +const instance: BaseResponseGrantedTokenInfo = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseGroupStudyMembersResponseContent.md b/src/api/openapi/docs/BaseResponseGroupStudyMembersResponseContent.md new file mode 100644 index 00000000..101826aa --- /dev/null +++ b/src/api/openapi/docs/BaseResponseGroupStudyMembersResponseContent.md @@ -0,0 +1,26 @@ +# BaseResponseGroupStudyMembersResponseContent + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**GroupStudyMembersResponseContent**](GroupStudyMembersResponseContent.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseGroupStudyMembersResponseContent } from './api'; + +const instance: BaseResponseGroupStudyMembersResponseContent = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseGroupStudyProgressGradesResponse.md b/src/api/openapi/docs/BaseResponseGroupStudyProgressGradesResponse.md new file mode 100644 index 00000000..b025efca --- /dev/null +++ b/src/api/openapi/docs/BaseResponseGroupStudyProgressGradesResponse.md @@ -0,0 +1,26 @@ +# BaseResponseGroupStudyProgressGradesResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**GroupStudyProgressGradesResponse**](GroupStudyProgressGradesResponse.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseGroupStudyProgressGradesResponse } from './api'; + +const instance: BaseResponseGroupStudyProgressGradesResponse = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseHomeworkSubmissionResponse.md b/src/api/openapi/docs/BaseResponseHomeworkSubmissionResponse.md new file mode 100644 index 00000000..b2012c4e --- /dev/null +++ b/src/api/openapi/docs/BaseResponseHomeworkSubmissionResponse.md @@ -0,0 +1,26 @@ +# BaseResponseHomeworkSubmissionResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**HomeworkSubmissionResponse**](HomeworkSubmissionResponse.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseHomeworkSubmissionResponse } from './api'; + +const instance: BaseResponseHomeworkSubmissionResponse = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseMatchingRequestResponse.md b/src/api/openapi/docs/BaseResponseMatchingRequestResponse.md new file mode 100644 index 00000000..0745b5c0 --- /dev/null +++ b/src/api/openapi/docs/BaseResponseMatchingRequestResponse.md @@ -0,0 +1,26 @@ +# BaseResponseMatchingRequestResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**MatchingRequestResponse**](MatchingRequestResponse.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseMatchingRequestResponse } from './api'; + +const instance: BaseResponseMatchingRequestResponse = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseMemberAccountHistoryResponseDto.md b/src/api/openapi/docs/BaseResponseMemberAccountHistoryResponseDto.md new file mode 100644 index 00000000..273b092c --- /dev/null +++ b/src/api/openapi/docs/BaseResponseMemberAccountHistoryResponseDto.md @@ -0,0 +1,26 @@ +# BaseResponseMemberAccountHistoryResponseDto + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**MemberAccountHistoryResponseDto**](MemberAccountHistoryResponseDto.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseMemberAccountHistoryResponseDto } from './api'; + +const instance: BaseResponseMemberAccountHistoryResponseDto = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseObject.md b/src/api/openapi/docs/BaseResponseObject.md new file mode 100644 index 00000000..39710f2d --- /dev/null +++ b/src/api/openapi/docs/BaseResponseObject.md @@ -0,0 +1,26 @@ +# BaseResponseObject + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | **object** | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseObject } from './api'; + +const instance: BaseResponseObject = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseString.md b/src/api/openapi/docs/BaseResponseString.md new file mode 100644 index 00000000..f84614fb --- /dev/null +++ b/src/api/openapi/docs/BaseResponseString.md @@ -0,0 +1,26 @@ +# BaseResponseString + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | **string** | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseString } from './api'; + +const instance: BaseResponseString = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseStudySpaceIsParticipateResponse.md b/src/api/openapi/docs/BaseResponseStudySpaceIsParticipateResponse.md new file mode 100644 index 00000000..da0bdd19 --- /dev/null +++ b/src/api/openapi/docs/BaseResponseStudySpaceIsParticipateResponse.md @@ -0,0 +1,26 @@ +# BaseResponseStudySpaceIsParticipateResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**StudySpaceIsParticipateResponse**](StudySpaceIsParticipateResponse.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseStudySpaceIsParticipateResponse } from './api'; + +const instance: BaseResponseStudySpaceIsParticipateResponse = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseTemporalToken.md b/src/api/openapi/docs/BaseResponseTemporalToken.md new file mode 100644 index 00000000..b08d73cb --- /dev/null +++ b/src/api/openapi/docs/BaseResponseTemporalToken.md @@ -0,0 +1,26 @@ +# BaseResponseTemporalToken + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | [**TemporalToken**](TemporalToken.md) | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseTemporalToken } from './api'; + +const instance: BaseResponseTemporalToken = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/BaseResponseVoid.md b/src/api/openapi/docs/BaseResponseVoid.md new file mode 100644 index 00000000..9b2b9949 --- /dev/null +++ b/src/api/openapi/docs/BaseResponseVoid.md @@ -0,0 +1,26 @@ +# BaseResponseVoid + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**statusCode** | **number** | Status Code | [default to undefined] +**timestamp** | **string** | Timestamp | [default to undefined] +**content** | **object** | Content | [optional] [default to undefined] +**message** | **string** | Message | [optional] [default to undefined] + +## Example + +```typescript +import { BaseResponseVoid } from './api'; + +const instance: BaseResponseVoid = { + statusCode, + timestamp, + content, + message, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/ChangeHistDto.md b/src/api/openapi/docs/ChangeHistDto.md new file mode 100644 index 00000000..37710de1 --- /dev/null +++ b/src/api/openapi/docs/ChangeHistDto.md @@ -0,0 +1,24 @@ +# ChangeHistDto + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**changedAt** | **string** | | [optional] [default to undefined] +**from** | **string** | | [optional] [default to undefined] +**to** | **string** | | [optional] [default to undefined] + +## Example + +```typescript +import { ChangeHistDto } from './api'; + +const instance: ChangeHistDto = { + changedAt, + from, + to, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/CursorResponseDtoStudyReservationMember.md b/src/api/openapi/docs/CursorResponseDtoStudyReservationMemberDto.md similarity index 61% rename from src/api/openapi/docs/CursorResponseDtoStudyReservationMember.md rename to src/api/openapi/docs/CursorResponseDtoStudyReservationMemberDto.md index 147f63d1..8746c30d 100644 --- a/src/api/openapi/docs/CursorResponseDtoStudyReservationMember.md +++ b/src/api/openapi/docs/CursorResponseDtoStudyReservationMemberDto.md @@ -1,20 +1,20 @@ -# CursorResponseDtoStudyReservationMember +# CursorResponseDtoStudyReservationMemberDto ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**items** | [**Array<StudyReservationMember>**](StudyReservationMember.md) | | [optional] [default to undefined] +**items** | [**Array<StudyReservationMemberDto>**](StudyReservationMemberDto.md) | | [optional] [default to undefined] **nextCursor** | **number** | | [optional] [default to undefined] **hasNext** | **boolean** | | [optional] [default to undefined] ## Example ```typescript -import { CursorResponseDtoStudyReservationMember } from './api'; +import { CursorResponseDtoStudyReservationMemberDto } from './api'; -const instance: CursorResponseDtoStudyReservationMember = { +const instance: CursorResponseDtoStudyReservationMemberDto = { items, nextCursor, hasNext, diff --git a/src/api/openapi/docs/FileControllerApi.md b/src/api/openapi/docs/FileControllerApi.md index 13b22d9b..9a0ece47 100644 --- a/src/api/openapi/docs/FileControllerApi.md +++ b/src/api/openapi/docs/FileControllerApi.md @@ -7,7 +7,7 @@ All URIs are relative to *https://test-api.zeroone.it.kr* |[**uploadProfileImage**](#uploadprofileimage) | **PUT** /api/v1/files/{filePath} | | # **uploadProfileImage** -> BaseResponse uploadProfileImage() +> BaseResponseVoid uploadProfileImage() ### Example @@ -44,7 +44,7 @@ const { status, data } = await apiInstance.uploadProfileImage( ### Return type -**BaseResponse** +**BaseResponseVoid** ### Authorization diff --git a/src/api/openapi/docs/GrantedTokenInfo.md b/src/api/openapi/docs/GrantedTokenInfo.md new file mode 100644 index 00000000..849fdb75 --- /dev/null +++ b/src/api/openapi/docs/GrantedTokenInfo.md @@ -0,0 +1,32 @@ +# GrantedTokenInfo + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**accessToken** | **string** | | [default to undefined] +**refreshToken** | **string** | | [optional] [default to undefined] +**id** | **string** | | [default to undefined] +**authVendor** | **string** | | [default to undefined] +**name** | **string** | | [optional] [default to undefined] +**profileImageUrl** | **string** | | [optional] [default to undefined] +**userInfo** | [**OAuth2UserInfo**](OAuth2UserInfo.md) | | [optional] [default to undefined] + +## Example + +```typescript +import { GrantedTokenInfo } from './api'; + +const instance: GrantedTokenInfo = { + accessToken, + refreshToken, + id, + authVendor, + name, + profileImageUrl, + userInfo, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/GroupStudyApplicant.md b/src/api/openapi/docs/GroupStudyApplicant.md index a9dc9fe8..5e87304f 100644 --- a/src/api/openapi/docs/GroupStudyApplicant.md +++ b/src/api/openapi/docs/GroupStudyApplicant.md @@ -8,7 +8,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **memberId** | **number** | 신청자 ID | [optional] [default to undefined] **memberNickname** | **string** | 신청자 닉네임 | [optional] [default to undefined] -**profileImage** | [**Image**](Image.md) | 신청자 프로필 이미지 | [optional] [default to undefined] +**profileImage** | [**ImageDto**](ImageDto.md) | 신청자 프로필 이미지 | [optional] [default to undefined] **simpleIntroduction** | **string** | 신청자 한줄 소개 | [optional] [default to undefined] **sincerityTemp** | [**SincerityTempResponse**](SincerityTempResponse.md) | 성실온도 | [optional] [default to undefined] diff --git a/src/api/openapi/docs/GroupStudyApplyDetailResponse.md b/src/api/openapi/docs/GroupStudyApplyDetailResponse.md index 847389bf..7bb10874 100644 --- a/src/api/openapi/docs/GroupStudyApplyDetailResponse.md +++ b/src/api/openapi/docs/GroupStudyApplyDetailResponse.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **statusCode** | **number** | Status Code | [default to undefined] **timestamp** | **string** | Timestamp | [default to undefined] -**content** | [**GroupStudyApplyResponseContent**](GroupStudyApplyResponseContent.md) | Content | [optional] [default to undefined] +**content** | [**GroupStudyApplyResponseDto**](GroupStudyApplyResponseDto.md) | Content | [optional] [default to undefined] **message** | **string** | Message | [optional] [default to undefined] ## Example diff --git a/src/api/openapi/docs/GroupStudyApplyResponseContent.md b/src/api/openapi/docs/GroupStudyApplyResponseDto.md similarity index 91% rename from src/api/openapi/docs/GroupStudyApplyResponseContent.md rename to src/api/openapi/docs/GroupStudyApplyResponseDto.md index f9cefebd..fb77ac6b 100644 --- a/src/api/openapi/docs/GroupStudyApplyResponseContent.md +++ b/src/api/openapi/docs/GroupStudyApplyResponseDto.md @@ -1,4 +1,4 @@ -# GroupStudyApplyResponseContent +# GroupStudyApplyResponseDto 그룹스터디 신청 응답 @@ -21,9 +21,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { GroupStudyApplyResponseContent } from './api'; +import { GroupStudyApplyResponseDto } from './api'; -const instance: GroupStudyApplyResponseContent = { +const instance: GroupStudyApplyResponseDto = { applyId, applicantId, groupStudyId, diff --git a/src/api/openapi/docs/GroupStudyBasicInfoResponse.md b/src/api/openapi/docs/GroupStudyBasicInfoResponseDto.md similarity index 89% rename from src/api/openapi/docs/GroupStudyBasicInfoResponse.md rename to src/api/openapi/docs/GroupStudyBasicInfoResponseDto.md index 44834a49..dc9f338a 100644 --- a/src/api/openapi/docs/GroupStudyBasicInfoResponse.md +++ b/src/api/openapi/docs/GroupStudyBasicInfoResponseDto.md @@ -1,6 +1,5 @@ -# GroupStudyBasicInfoResponse +# GroupStudyBasicInfoResponseDto -그룹스터디 기본 정보 응답 ## Properties @@ -8,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **groupStudyId** | **number** | 그룹스터디 ID | [optional] [default to undefined] **classification** | **string** | 스터디 분류 | [optional] [default to undefined] -**leader** | [**StudyReservationMember**](StudyReservationMember.md) | 그룹스터디 리더 정보 | [optional] [default to undefined] +**leader** | [**StudyReservationMemberDto**](StudyReservationMemberDto.md) | 그룹스터디 리더 정보 | [optional] [default to undefined] **type** | **string** | 스터디 타입 | [optional] [default to undefined] **hostType** | **string** | 스터디 주최자 구분 | [optional] [default to undefined] **targetRoles** | **Array<string>** | 스터디 모집 대상 (복수 선택 가능) | [optional] [default to undefined] @@ -32,9 +31,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { GroupStudyBasicInfoResponse } from './api'; +import { GroupStudyBasicInfoResponseDto } from './api'; -const instance: GroupStudyBasicInfoResponse = { +const instance: GroupStudyBasicInfoResponseDto = { groupStudyId, classification, leader, diff --git a/src/api/openapi/docs/GroupStudyCreationResponse.md b/src/api/openapi/docs/GroupStudyCreationResponse.md index 2218fee4..ad77c6e4 100644 --- a/src/api/openapi/docs/GroupStudyCreationResponse.md +++ b/src/api/openapi/docs/GroupStudyCreationResponse.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **statusCode** | **number** | Status Code | [default to undefined] **timestamp** | **string** | Timestamp | [default to undefined] -**content** | [**GroupStudyCreationResponseContent**](GroupStudyCreationResponseContent.md) | Content | [optional] [default to undefined] +**content** | [**GroupStudyCreationResponseDto**](GroupStudyCreationResponseDto.md) | Content | [optional] [default to undefined] **message** | **string** | Message | [optional] [default to undefined] ## Example diff --git a/src/api/openapi/docs/GroupStudyCreationResponseContent.md b/src/api/openapi/docs/GroupStudyCreationResponseDto.md similarity index 82% rename from src/api/openapi/docs/GroupStudyCreationResponseContent.md rename to src/api/openapi/docs/GroupStudyCreationResponseDto.md index 091c5cb5..b513a2f6 100644 --- a/src/api/openapi/docs/GroupStudyCreationResponseContent.md +++ b/src/api/openapi/docs/GroupStudyCreationResponseDto.md @@ -1,6 +1,5 @@ -# GroupStudyCreationResponseContent +# GroupStudyCreationResponseDto -그룹스터디 생성 응답 ## Properties @@ -15,9 +14,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { GroupStudyCreationResponseContent } from './api'; +import { GroupStudyCreationResponseDto } from './api'; -const instance: GroupStudyCreationResponseContent = { +const instance: GroupStudyCreationResponseDto = { groupStudyId, thumbnailUploadUrl, createdAt, diff --git a/src/api/openapi/docs/GroupStudyDetailInfoResponse.md b/src/api/openapi/docs/GroupStudyDetailInfoResponseDto.md similarity index 79% rename from src/api/openapi/docs/GroupStudyDetailInfoResponse.md rename to src/api/openapi/docs/GroupStudyDetailInfoResponseDto.md index 918513bb..168193ab 100644 --- a/src/api/openapi/docs/GroupStudyDetailInfoResponse.md +++ b/src/api/openapi/docs/GroupStudyDetailInfoResponseDto.md @@ -1,12 +1,11 @@ -# GroupStudyDetailInfoResponse +# GroupStudyDetailInfoResponseDto -그룹스터디 상세 정보 응답 ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**image** | [**Image**](Image.md) | 기존 스터디 썸네일 이미지 | [optional] [default to undefined] +**image** | [**ImageDto**](ImageDto.md) | 기존 스터디 썸네일 이미지 | [optional] [default to undefined] **title** | **string** | 스터디 제목 | [optional] [default to undefined] **description** | **string** | 스터디 소개 | [optional] [default to undefined] **summary** | **string** | 스터디 한줄 요약 | [optional] [default to undefined] @@ -18,9 +17,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { GroupStudyDetailInfoResponse } from './api'; +import { GroupStudyDetailInfoResponseDto } from './api'; -const instance: GroupStudyDetailInfoResponse = { +const instance: GroupStudyDetailInfoResponseDto = { image, title, description, diff --git a/src/api/openapi/docs/GroupStudyDetailResponse.md b/src/api/openapi/docs/GroupStudyDetailResponse.md index cc146bc3..e04362ca 100644 --- a/src/api/openapi/docs/GroupStudyDetailResponse.md +++ b/src/api/openapi/docs/GroupStudyDetailResponse.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **statusCode** | **number** | Status Code | [default to undefined] **timestamp** | **string** | Timestamp | [default to undefined] -**content** | [**GroupStudyDetailResponseContent**](GroupStudyDetailResponseContent.md) | Content | [optional] [default to undefined] +**content** | [**GroupStudyFullResponseDto**](GroupStudyFullResponseDto.md) | Content | [optional] [default to undefined] **message** | **string** | Message | [optional] [default to undefined] ## Example diff --git a/src/api/openapi/docs/GroupStudyDetailResponseContent.md b/src/api/openapi/docs/GroupStudyDetailResponseContent.md deleted file mode 100644 index f1a965ef..00000000 --- a/src/api/openapi/docs/GroupStudyDetailResponseContent.md +++ /dev/null @@ -1,25 +0,0 @@ -# GroupStudyDetailResponseContent - -그룹스터디 상세 조회 응답 - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**basicInfo** | [**GroupStudyBasicInfoResponse**](GroupStudyBasicInfoResponse.md) | 그룹스터디 기본 정보 | [optional] [default to undefined] -**detailInfo** | [**GroupStudyDetailInfoResponse**](GroupStudyDetailInfoResponse.md) | 그룹스터디 상세 정보 | [optional] [default to undefined] -**interviewPost** | [**GroupStudInterviewPostResponseContent**](GroupStudInterviewPostResponseContent.md) | 그룹스터디 면접 질문 | [optional] [default to undefined] - -## Example - -```typescript -import { GroupStudyDetailResponseContent } from './api'; - -const instance: GroupStudyDetailResponseContent = { - basicInfo, - detailInfo, - interviewPost, -}; -``` - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/GroupStudyFullResponseDto.md b/src/api/openapi/docs/GroupStudyFullResponseDto.md new file mode 100644 index 00000000..d04eca68 --- /dev/null +++ b/src/api/openapi/docs/GroupStudyFullResponseDto.md @@ -0,0 +1,24 @@ +# GroupStudyFullResponseDto + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**basicInfo** | [**GroupStudyBasicInfoResponseDto**](GroupStudyBasicInfoResponseDto.md) | 그룹스터디 기본 정보 | [optional] [default to undefined] +**detailInfo** | [**GroupStudyDetailInfoResponseDto**](GroupStudyDetailInfoResponseDto.md) | 그룹스터디 상세 정보 | [optional] [default to undefined] +**interviewPost** | [**GroupStudyInterviewPostResponseDto**](GroupStudyInterviewPostResponseDto.md) | 그룹스터디 면접 질문 | [optional] [default to undefined] + +## Example + +```typescript +import { GroupStudyFullResponseDto } from './api'; + +const instance: GroupStudyFullResponseDto = { + basicInfo, + detailInfo, + interviewPost, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/GroupStudyHomeworkApi.md b/src/api/openapi/docs/GroupStudyHomeworkApi.md index 0328c710..bd4a980b 100644 --- a/src/api/openapi/docs/GroupStudyHomeworkApi.md +++ b/src/api/openapi/docs/GroupStudyHomeworkApi.md @@ -8,7 +8,7 @@ All URIs are relative to *https://test-api.zeroone.it.kr* |[**submitHomework**](#submithomework) | **POST** /missions/{missionId}/homeworks | | # **editHomework** -> BaseResponse editHomework(homeworkEditRequest) +> BaseResponseVoid editHomework(homeworkEditRequest) ### Example @@ -42,7 +42,7 @@ const { status, data } = await apiInstance.editHomework( ### Return type -**BaseResponse** +**BaseResponseVoid** ### Authorization @@ -62,7 +62,7 @@ const { status, data } = await apiInstance.editHomework( [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **submitHomework** -> BaseResponse submitHomework(homeworkSubmissionRequest) +> BaseResponseHomeworkSubmissionResponse submitHomework(homeworkSubmissionRequest) ### Example @@ -96,7 +96,7 @@ const { status, data } = await apiInstance.submitHomework( ### Return type -**BaseResponse** +**BaseResponseHomeworkSubmissionResponse** ### Authorization diff --git a/src/api/openapi/docs/GroupStudInterviewPostResponseContent.md b/src/api/openapi/docs/GroupStudyInterviewPostResponseDto.md similarity index 83% rename from src/api/openapi/docs/GroupStudInterviewPostResponseContent.md rename to src/api/openapi/docs/GroupStudyInterviewPostResponseDto.md index 9368fb75..1e25a5af 100644 --- a/src/api/openapi/docs/GroupStudInterviewPostResponseContent.md +++ b/src/api/openapi/docs/GroupStudyInterviewPostResponseDto.md @@ -1,4 +1,4 @@ -# GroupStudInterviewPostResponseContent +# GroupStudyInterviewPostResponseDto 그룹스터디 개설질문 응답 @@ -14,9 +14,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { GroupStudInterviewPostResponseContent } from './api'; +import { GroupStudyInterviewPostResponseDto } from './api'; -const instance: GroupStudInterviewPostResponseContent = { +const instance: GroupStudyInterviewPostResponseDto = { interviewPost, createdAt, updatedAt, diff --git a/src/api/openapi/docs/GroupStudyInterviewPostUpdateResponse.md b/src/api/openapi/docs/GroupStudyInterviewPostUpdateResponse.md index f930fd45..6bc975b8 100644 --- a/src/api/openapi/docs/GroupStudyInterviewPostUpdateResponse.md +++ b/src/api/openapi/docs/GroupStudyInterviewPostUpdateResponse.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **statusCode** | **number** | Status Code | [default to undefined] **timestamp** | **string** | Timestamp | [default to undefined] -**content** | [**GroupStudInterviewPostResponseContent**](GroupStudInterviewPostResponseContent.md) | Content | [optional] [default to undefined] +**content** | [**GroupStudyInterviewPostResponseDto**](GroupStudyInterviewPostResponseDto.md) | Content | [optional] [default to undefined] **message** | **string** | Message | [optional] [default to undefined] ## Example diff --git a/src/api/openapi/docs/GroupStudyListItem.md b/src/api/openapi/docs/GroupStudyListItem.md deleted file mode 100644 index f6cceb5c..00000000 --- a/src/api/openapi/docs/GroupStudyListItem.md +++ /dev/null @@ -1,23 +0,0 @@ -# GroupStudyListItem - -그룹스터디 목록 아이템 (PageResponse에서 사용) - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**basicInfo** | [**GroupStudyBasicInfoResponse**](GroupStudyBasicInfoResponse.md) | 그룹스터디 기본 정보 | [optional] [default to undefined] -**simpleDetailInfo** | [**GroupStudySimpleInfoResponse**](GroupStudySimpleInfoResponse.md) | 그룹스터디 간단한 상세 정보 | [optional] [default to undefined] - -## Example - -```typescript -import { GroupStudyListItem } from './api'; - -const instance: GroupStudyListItem = { - basicInfo, - simpleDetailInfo, -}; -``` - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/GroupStudyListItemDto.md b/src/api/openapi/docs/GroupStudyListItemDto.md new file mode 100644 index 00000000..bc828d00 --- /dev/null +++ b/src/api/openapi/docs/GroupStudyListItemDto.md @@ -0,0 +1,23 @@ +# GroupStudyListItemDto + +그룹스터디 목록 아이템 (PageResponse에서 사용) + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**basicInfo** | [**GroupStudyBasicInfoResponseDto**](GroupStudyBasicInfoResponseDto.md) | 그룹스터디 기본 정보 | [optional] [default to undefined] +**simpleDetailInfo** | [**GroupStudySimpleInfoResponseDto**](GroupStudySimpleInfoResponseDto.md) | 그룹스터디 간단한 상세 정보 | [optional] [default to undefined] + +## Example + +```typescript +import { GroupStudyListItemDto } from './api'; + +const instance: GroupStudyListItemDto = { + basicInfo, + simpleDetailInfo, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/GroupStudyListResponse.md b/src/api/openapi/docs/GroupStudyListResponse.md index 21d49260..7cbe5e65 100644 --- a/src/api/openapi/docs/GroupStudyListResponse.md +++ b/src/api/openapi/docs/GroupStudyListResponse.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **statusCode** | **number** | Status Code | [default to undefined] **timestamp** | **string** | Timestamp | [default to undefined] -**content** | [**PageResponseGroupStudyListItem**](PageResponseGroupStudyListItem.md) | Content | [optional] [default to undefined] +**content** | [**PageResponseGroupStudyListItemDto**](PageResponseGroupStudyListItemDto.md) | Content | [optional] [default to undefined] **message** | **string** | Message | [optional] [default to undefined] ## Example diff --git a/src/api/openapi/docs/GroupStudyProgressGradeResponse.md b/src/api/openapi/docs/GroupStudyProgressGradeResponse.md new file mode 100644 index 00000000..554ce76f --- /dev/null +++ b/src/api/openapi/docs/GroupStudyProgressGradeResponse.md @@ -0,0 +1,26 @@ +# GroupStudyProgressGradeResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **number** | | [optional] [default to undefined] +**code** | **string** | | [optional] [default to undefined] +**name** | **string** | | [optional] [default to undefined] +**score** | **number** | | [optional] [default to undefined] + +## Example + +```typescript +import { GroupStudyProgressGradeResponse } from './api'; + +const instance: GroupStudyProgressGradeResponse = { + id, + code, + name, + score, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/GroupStudyProgressGradesResponse.md b/src/api/openapi/docs/GroupStudyProgressGradesResponse.md new file mode 100644 index 00000000..53801f1c --- /dev/null +++ b/src/api/openapi/docs/GroupStudyProgressGradesResponse.md @@ -0,0 +1,20 @@ +# GroupStudyProgressGradesResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**grades** | [**Array<GroupStudyProgressGradeResponse>**](GroupStudyProgressGradeResponse.md) | | [optional] [default to undefined] + +## Example + +```typescript +import { GroupStudyProgressGradesResponse } from './api'; + +const instance: GroupStudyProgressGradesResponse = { + grades, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/GroupStudySimpleInfoResponse.md b/src/api/openapi/docs/GroupStudySimpleInfoResponseDto.md similarity index 69% rename from src/api/openapi/docs/GroupStudySimpleInfoResponse.md rename to src/api/openapi/docs/GroupStudySimpleInfoResponseDto.md index 41d421d7..f33e6899 100644 --- a/src/api/openapi/docs/GroupStudySimpleInfoResponse.md +++ b/src/api/openapi/docs/GroupStudySimpleInfoResponseDto.md @@ -1,4 +1,4 @@ -# GroupStudySimpleInfoResponse +# GroupStudySimpleInfoResponseDto 그룹스터디 간단한 상세 정보 (목록조회용) @@ -6,16 +6,16 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**thumbnail** | [**Image**](Image.md) | 스터디 썸네일 이미지 | [optional] [default to undefined] +**thumbnail** | [**ImageDto**](ImageDto.md) | 스터디 썸네일 이미지 | [optional] [default to undefined] **title** | **string** | 스터디 제목 | [optional] [default to undefined] **summary** | **string** | 스터디 한줄 요약 | [optional] [default to undefined] ## Example ```typescript -import { GroupStudySimpleInfoResponse } from './api'; +import { GroupStudySimpleInfoResponseDto } from './api'; -const instance: GroupStudySimpleInfoResponse = { +const instance: GroupStudySimpleInfoResponseDto = { thumbnail, title, summary, diff --git a/src/api/openapi/docs/HomeworkSubmissionResponse.md b/src/api/openapi/docs/HomeworkSubmissionResponse.md new file mode 100644 index 00000000..857d4cc6 --- /dev/null +++ b/src/api/openapi/docs/HomeworkSubmissionResponse.md @@ -0,0 +1,20 @@ +# HomeworkSubmissionResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**homeworkId** | **number** | | [optional] [default to undefined] + +## Example + +```typescript +import { HomeworkSubmissionResponse } from './api'; + +const instance: HomeworkSubmissionResponse = { + homeworkId, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/Image.md b/src/api/openapi/docs/ImageDto.md similarity index 60% rename from src/api/openapi/docs/Image.md rename to src/api/openapi/docs/ImageDto.md index a164ea17..4713c242 100644 --- a/src/api/openapi/docs/Image.md +++ b/src/api/openapi/docs/ImageDto.md @@ -1,4 +1,4 @@ -# Image +# ImageDto ## Properties @@ -6,14 +6,14 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **imageId** | **number** | 이미지 ID | [optional] [default to undefined] -**resizedImages** | [**Array<ResizedImage>**](ResizedImage.md) | 같은 이미지를 여러 사이즈로 리사이징하여 생성된 이미지 목록 | [optional] [default to undefined] +**resizedImages** | [**Array<ResizedImageDto>**](ResizedImageDto.md) | 같은 이미지를 여러 사이즈로 리사이징하여 생성된 이미지 목록 | [optional] [default to undefined] ## Example ```typescript -import { Image } from './api'; +import { ImageDto } from './api'; -const instance: Image = { +const instance: ImageDto = { imageId, resizedImages, }; diff --git a/src/api/openapi/docs/MatchingRequestResponse.md b/src/api/openapi/docs/MatchingRequestResponse.md new file mode 100644 index 00000000..69b12c4e --- /dev/null +++ b/src/api/openapi/docs/MatchingRequestResponse.md @@ -0,0 +1,34 @@ +# MatchingRequestResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**matchingRequestId** | **number** | 매칭 요청 ID | [optional] [default to undefined] +**memberId** | **number** | 요청 회원 ID | [optional] [default to undefined] +**partnerId** | **number** | 파트너 회원 ID | [optional] [default to undefined] +**status** | **string** | 매칭 상태 | [optional] [default to undefined] +**type** | **string** | 매칭 종류 | [optional] [default to undefined] +**content** | **string** | 내용/메모 | [optional] [default to undefined] +**createdAt** | **string** | 생성 일시 | [optional] [default to undefined] +**updatedAt** | **string** | 수정 일시 | [optional] [default to undefined] + +## Example + +```typescript +import { MatchingRequestResponse } from './api'; + +const instance: MatchingRequestResponse = { + matchingRequestId, + memberId, + partnerId, + status, + type, + content, + createdAt, + updatedAt, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/MemberAccountHistoryResponseDto.md b/src/api/openapi/docs/MemberAccountHistoryResponseDto.md new file mode 100644 index 00000000..eecaf64c --- /dev/null +++ b/src/api/openapi/docs/MemberAccountHistoryResponseDto.md @@ -0,0 +1,30 @@ +# MemberAccountHistoryResponseDto + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**memberId** | **number** | | [optional] [default to undefined] +**joinedAt** | **string** | | [optional] [default to undefined] +**loginMostRecentlyAt** | **string** | | [optional] [default to undefined] +**loginHists** | **Array<string>** | | [optional] [default to undefined] +**roleChangeHists** | [**Array<ChangeHistDto>**](ChangeHistDto.md) | | [optional] [default to undefined] +**memberStatusChangeHists** | [**Array<ChangeHistDto>**](ChangeHistDto.md) | | [optional] [default to undefined] + +## Example + +```typescript +import { MemberAccountHistoryResponseDto } from './api'; + +const instance: MemberAccountHistoryResponseDto = { + memberId, + joinedAt, + loginMostRecentlyAt, + loginHists, + roleChangeHists, + memberStatusChangeHists, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/MemberProfileResponseDto.md b/src/api/openapi/docs/MemberProfileResponseDto.md index be7b36b7..4c45ad60 100644 --- a/src/api/openapi/docs/MemberProfileResponseDto.md +++ b/src/api/openapi/docs/MemberProfileResponseDto.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **memberName** | **string** | 회원 이름 | [optional] [default to undefined] **nickname** | **string** | 닉네임 | [optional] [default to undefined] -**profileImage** | [**Image**](Image.md) | 프로필 이미지 - 리사이징된 이미지를 포함하고 있음 - 지금은 ORIGINAL 하나밖에 없음 | [optional] [default to undefined] +**profileImage** | [**ImageDto**](ImageDto.md) | 프로필 이미지 - 리사이징된 이미지를 포함하고 있음 - 지금은 ORIGINAL 하나밖에 없음 | [optional] [default to undefined] **simpleIntroduction** | **string** | 한마디 소개 | [optional] [default to undefined] **mbti** | **string** | MBTI | [optional] [default to undefined] **interests** | [**Array<IdNameDto>**](IdNameDto.md) | 관심사 | [optional] [default to undefined] diff --git a/src/api/openapi/docs/MemberTmpControllerApi.md b/src/api/openapi/docs/MemberTmpControllerApi.md index f96041e6..8a257118 100644 --- a/src/api/openapi/docs/MemberTmpControllerApi.md +++ b/src/api/openapi/docs/MemberTmpControllerApi.md @@ -7,7 +7,7 @@ All URIs are relative to *https://test-api.zeroone.it.kr* |[**deleteMemberPermanently**](#deletememberpermanently) | **GET** /api/v1/members/{memberId}/permanently | | # **deleteMemberPermanently** -> BaseResponse deleteMemberPermanently() +> BaseResponseVoid deleteMemberPermanently() ### Example @@ -37,7 +37,7 @@ const { status, data } = await apiInstance.deleteMemberPermanently( ### Return type -**BaseResponse** +**BaseResponseVoid** ### Authorization diff --git a/src/api/openapi/docs/MyGroupStudyApplicationResponse.md b/src/api/openapi/docs/MyGroupStudyApplicationResponse.md index 13c9d9d3..b79365e1 100644 --- a/src/api/openapi/docs/MyGroupStudyApplicationResponse.md +++ b/src/api/openapi/docs/MyGroupStudyApplicationResponse.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **statusCode** | **number** | Status Code | [default to undefined] **timestamp** | **string** | Timestamp | [default to undefined] -**content** | [**PageResponseMyGroupStudyApplyListItem**](PageResponseMyGroupStudyApplyListItem.md) | Content | [optional] [default to undefined] +**content** | [**PageResponseMyGroupStudyApplyListItemDto**](PageResponseMyGroupStudyApplyListItemDto.md) | Content | [optional] [default to undefined] **message** | **string** | Message | [optional] [default to undefined] ## Example diff --git a/src/api/openapi/docs/MyGroupStudyApplyListItem.md b/src/api/openapi/docs/MyGroupStudyApplyListItem.md deleted file mode 100644 index 94d096fe..00000000 --- a/src/api/openapi/docs/MyGroupStudyApplyListItem.md +++ /dev/null @@ -1,27 +0,0 @@ -# MyGroupStudyApplyListItem - -내가 신청한 그룹스터디 목록 아이템 (PageResponse에서 사용) - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**basicInfo** | [**GroupStudyBasicInfoResponse**](GroupStudyBasicInfoResponse.md) | 그룹스터디 기본 정보 | [optional] [default to undefined] -**simpleDetailInfo** | [**GroupStudySimpleInfoResponse**](GroupStudySimpleInfoResponse.md) | 그룹스터디 간단한 상세 정보 | [optional] [default to undefined] -**applyInfo** | [**GroupStudyApplyResponseContent**](GroupStudyApplyResponseContent.md) | 그룹스터디 신청 정보 | [optional] [default to undefined] -**reviewWritten** | **boolean** | 후기 작성 여부 | [optional] [default to undefined] - -## Example - -```typescript -import { MyGroupStudyApplyListItem } from './api'; - -const instance: MyGroupStudyApplyListItem = { - basicInfo, - simpleDetailInfo, - applyInfo, - reviewWritten, -}; -``` - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/MyGroupStudyApplyListItemDto.md b/src/api/openapi/docs/MyGroupStudyApplyListItemDto.md new file mode 100644 index 00000000..a89a9898 --- /dev/null +++ b/src/api/openapi/docs/MyGroupStudyApplyListItemDto.md @@ -0,0 +1,27 @@ +# MyGroupStudyApplyListItemDto + +내가 신청한 그룹스터디 목록 아이템 (PageResponse에서 사용) + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**basicInfo** | [**GroupStudyBasicInfoResponseDto**](GroupStudyBasicInfoResponseDto.md) | 그룹스터디 기본 정보 | [optional] [default to undefined] +**simpleDetailInfo** | [**GroupStudySimpleInfoResponseDto**](GroupStudySimpleInfoResponseDto.md) | 그룹스터디 간단한 상세 정보 | [optional] [default to undefined] +**applyInfo** | [**GroupStudyApplyResponseDto**](GroupStudyApplyResponseDto.md) | 그룹스터디 신청 정보 | [optional] [default to undefined] +**reviewWritten** | **boolean** | 후기 작성 여부 | [optional] [default to undefined] + +## Example + +```typescript +import { MyGroupStudyApplyListItemDto } from './api'; + +const instance: MyGroupStudyApplyListItemDto = { + basicInfo, + simpleDetailInfo, + applyInfo, + reviewWritten, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/OAuth2UserInfo.md b/src/api/openapi/docs/OAuth2UserInfo.md new file mode 100644 index 00000000..ed57334c --- /dev/null +++ b/src/api/openapi/docs/OAuth2UserInfo.md @@ -0,0 +1,24 @@ +# OAuth2UserInfo + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **string** | | [optional] [default to undefined] +**name** | **string** | | [optional] [default to undefined] +**profileImageUrl** | **string** | | [optional] [default to undefined] + +## Example + +```typescript +import { OAuth2UserInfo } from './api'; + +const instance: OAuth2UserInfo = { + id, + name, + profileImageUrl, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/PageResponseGroupStudyListItem.md b/src/api/openapi/docs/PageResponseGroupStudyListItemDto.md similarity index 74% rename from src/api/openapi/docs/PageResponseGroupStudyListItem.md rename to src/api/openapi/docs/PageResponseGroupStudyListItemDto.md index f98f87b0..bf5da09d 100644 --- a/src/api/openapi/docs/PageResponseGroupStudyListItem.md +++ b/src/api/openapi/docs/PageResponseGroupStudyListItemDto.md @@ -1,11 +1,11 @@ -# PageResponseGroupStudyListItem +# PageResponseGroupStudyListItemDto ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**content** | [**Array<GroupStudyListItem>**](GroupStudyListItem.md) | | [optional] [default to undefined] +**content** | [**Array<GroupStudyListItemDto>**](GroupStudyListItemDto.md) | | [optional] [default to undefined] **page** | **number** | | [optional] [default to undefined] **size** | **number** | | [optional] [default to undefined] **totalElements** | **number** | | [optional] [default to undefined] @@ -16,9 +16,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { PageResponseGroupStudyListItem } from './api'; +import { PageResponseGroupStudyListItemDto } from './api'; -const instance: PageResponseGroupStudyListItem = { +const instance: PageResponseGroupStudyListItemDto = { content, page, size, diff --git a/src/api/openapi/docs/PageResponseMyGroupStudyApplyListItem.md b/src/api/openapi/docs/PageResponseMyGroupStudyApplyListItemDto.md similarity index 72% rename from src/api/openapi/docs/PageResponseMyGroupStudyApplyListItem.md rename to src/api/openapi/docs/PageResponseMyGroupStudyApplyListItemDto.md index 0ca076e3..fa6c8a73 100644 --- a/src/api/openapi/docs/PageResponseMyGroupStudyApplyListItem.md +++ b/src/api/openapi/docs/PageResponseMyGroupStudyApplyListItemDto.md @@ -1,11 +1,11 @@ -# PageResponseMyGroupStudyApplyListItem +# PageResponseMyGroupStudyApplyListItemDto ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**content** | [**Array<MyGroupStudyApplyListItem>**](MyGroupStudyApplyListItem.md) | | [optional] [default to undefined] +**content** | [**Array<MyGroupStudyApplyListItemDto>**](MyGroupStudyApplyListItemDto.md) | | [optional] [default to undefined] **page** | **number** | | [optional] [default to undefined] **size** | **number** | | [optional] [default to undefined] **totalElements** | **number** | | [optional] [default to undefined] @@ -16,9 +16,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { PageResponseMyGroupStudyApplyListItem } from './api'; +import { PageResponseMyGroupStudyApplyListItemDto } from './api'; -const instance: PageResponseMyGroupStudyApplyListItem = { +const instance: PageResponseMyGroupStudyApplyListItemDto = { content, page, size, diff --git a/src/api/openapi/docs/ParticipatingStudyInfo.md b/src/api/openapi/docs/ParticipatingStudyInfo.md index 33caf4b8..4f32010f 100644 --- a/src/api/openapi/docs/ParticipatingStudyInfo.md +++ b/src/api/openapi/docs/ParticipatingStudyInfo.md @@ -6,7 +6,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **studyId** | **number** | 스터디 ID. 그룹스터디 ID 혹은 1대1 스터디 ID. type에 따라서 종류가 달라진다 | [optional] [default to undefined] -**thumbnail** | [**Image**](Image.md) | 섬네일 이미지 | [optional] [default to undefined] +**thumbnail** | [**ImageDto**](ImageDto.md) | 섬네일 이미지 | [optional] [default to undefined] **title** | **string** | 스터디 제목 - 일대일 스터디인 경우 null | [optional] [default to undefined] **maxMembersCount** | **number** | 참여 가능한 최대 회원 수 | [optional] [default to undefined] **participantsCount** | **number** | 참여한 회원의 수 | [optional] [default to undefined] diff --git a/src/api/openapi/docs/ResizedImage.md b/src/api/openapi/docs/ResizedImageDto.md similarity index 87% rename from src/api/openapi/docs/ResizedImage.md rename to src/api/openapi/docs/ResizedImageDto.md index dcfda30e..c82dc9f4 100644 --- a/src/api/openapi/docs/ResizedImage.md +++ b/src/api/openapi/docs/ResizedImageDto.md @@ -1,4 +1,4 @@ -# ResizedImage +# ResizedImageDto ## Properties @@ -12,9 +12,9 @@ Name | Type | Description | Notes ## Example ```typescript -import { ResizedImage } from './api'; +import { ResizedImageDto } from './api'; -const instance: ResizedImage = { +const instance: ResizedImageDto = { resizedImageId, resizedImageUrl, imageSizeType, diff --git a/src/api/openapi/docs/SettlementAccountApi.md b/src/api/openapi/docs/SettlementAccountApi.md index 28a9157b..05d8081f 100644 --- a/src/api/openapi/docs/SettlementAccountApi.md +++ b/src/api/openapi/docs/SettlementAccountApi.md @@ -10,7 +10,7 @@ All URIs are relative to *https://test-api.zeroone.it.kr* |[**update**](#update) | **PUT** /api/v1/mypage/settlement-account | 정산 계좌 수정| # **_delete** -> BaseResponse _delete() +> BaseResponseVoid _delete() 작성일자: 2025-12-11 작성자: 이도현 --- ## Description - 로그인한 회원의 정산 계좌 정보를 삭제합니다. - 삭제 후 정산 계좌는 조회할 수 없습니다. --- ## Response - 성공 시 응답 바디 없이 204 No Content를 반환합니다. @@ -34,7 +34,7 @@ This endpoint does not have any parameters. ### Return type -**BaseResponse** +**BaseResponseVoid** ### Authorization diff --git a/src/api/openapi/docs/StudyReservationMember.md b/src/api/openapi/docs/StudyReservationMemberDto.md similarity index 73% rename from src/api/openapi/docs/StudyReservationMember.md rename to src/api/openapi/docs/StudyReservationMemberDto.md index ee3d578f..9083db86 100644 --- a/src/api/openapi/docs/StudyReservationMember.md +++ b/src/api/openapi/docs/StudyReservationMemberDto.md @@ -1,4 +1,4 @@ -# StudyReservationMember +# StudyReservationMemberDto ## Properties @@ -7,15 +7,15 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **memberId** | **number** | | [optional] [default to undefined] **memberNickname** | **string** | | [optional] [default to undefined] -**profileImage** | [**Image**](Image.md) | | [optional] [default to undefined] +**profileImage** | [**ImageDto**](ImageDto.md) | | [optional] [default to undefined] **simpleIntroduction** | **string** | | [optional] [default to undefined] ## Example ```typescript -import { StudyReservationMember } from './api'; +import { StudyReservationMemberDto } from './api'; -const instance: StudyReservationMember = { +const instance: StudyReservationMemberDto = { memberId, memberNickname, profileImage, diff --git a/src/api/openapi/docs/StudyReservationResponseContent.md b/src/api/openapi/docs/StudyReservationResponseContent.md index d7db5fa4..1e6c3e00 100644 --- a/src/api/openapi/docs/StudyReservationResponseContent.md +++ b/src/api/openapi/docs/StudyReservationResponseContent.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**members** | [**CursorResponseDtoStudyReservationMember**](CursorResponseDtoStudyReservationMember.md) | | [optional] [default to undefined] +**members** | [**CursorResponseDtoStudyReservationMemberDto**](CursorResponseDtoStudyReservationMemberDto.md) | | [optional] [default to undefined] **totalMemberCount** | **number** | | [optional] [default to undefined] ## Example diff --git a/src/api/openapi/docs/TemporalToken.md b/src/api/openapi/docs/TemporalToken.md new file mode 100644 index 00000000..5a3b54c2 --- /dev/null +++ b/src/api/openapi/docs/TemporalToken.md @@ -0,0 +1,20 @@ +# TemporalToken + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**accessToken** | **string** | | [optional] [default to undefined] + +## Example + +```typescript +import { TemporalToken } from './api'; + +const instance: TemporalToken = { + accessToken, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/ThreadCommentResponseContent.md b/src/api/openapi/docs/ThreadCommentResponseContent.md index 963df4f7..02dfadfd 100644 --- a/src/api/openapi/docs/ThreadCommentResponseContent.md +++ b/src/api/openapi/docs/ThreadCommentResponseContent.md @@ -9,7 +9,7 @@ Name | Type | Description | Notes **threadId** | **number** | | [optional] [default to undefined] **authorId** | **number** | | [optional] [default to undefined] **authorName** | **string** | | [optional] [default to undefined] -**image** | [**Image**](Image.md) | | [optional] [default to undefined] +**image** | [**ImageDto**](ImageDto.md) | | [optional] [default to undefined] **content** | **string** | | [optional] [default to undefined] **isLeader** | **boolean** | | [optional] [default to undefined] **likesCount** | **number** | | [optional] [default to undefined] diff --git a/src/api/openapi/docs/ThreadSummaryResponseContent.md b/src/api/openapi/docs/ThreadSummaryResponseContent.md index 4ba2d38f..84c318b8 100644 --- a/src/api/openapi/docs/ThreadSummaryResponseContent.md +++ b/src/api/openapi/docs/ThreadSummaryResponseContent.md @@ -9,7 +9,7 @@ Name | Type | Description | Notes **groupStudyId** | **number** | | [optional] [default to undefined] **authorId** | **number** | | [optional] [default to undefined] **authorName** | **string** | | [optional] [default to undefined] -**image** | [**Image**](Image.md) | | [optional] [default to undefined] +**image** | [**ImageDto**](ImageDto.md) | | [optional] [default to undefined] **content** | **string** | | [optional] [default to undefined] **isLeader** | **boolean** | | [optional] [default to undefined] **likesCount** | **number** | | [optional] [default to undefined] diff --git a/src/api/openapi/docs/TokenAPIApi.md b/src/api/openapi/docs/TokenAPIApi.md index 30847308..a5b37494 100644 --- a/src/api/openapi/docs/TokenAPIApi.md +++ b/src/api/openapi/docs/TokenAPIApi.md @@ -7,7 +7,7 @@ All URIs are relative to *https://test-api.zeroone.it.kr* |[**generateToken**](#generatetoken) | **GET** /api/v1/tokens/token | | # **generateToken** -> BaseResponse generateToken() +> BaseResponseTemporalToken generateToken() ### Example @@ -40,7 +40,7 @@ const { status, data } = await apiInstance.generateToken( ### Return type -**BaseResponse** +**BaseResponseTemporalToken** ### Authorization diff --git a/src/api/openapi/models/base-response-granted-token-info.ts b/src/api/openapi/models/base-response-granted-token-info.ts new file mode 100644 index 00000000..adfa320a --- /dev/null +++ b/src/api/openapi/models/base-response-granted-token-info.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GrantedTokenInfo } from './granted-token-info'; + +export interface BaseResponseGrantedTokenInfo { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: GrantedTokenInfo; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-group-study-members-response-content.ts b/src/api/openapi/models/base-response-group-study-members-response-content.ts new file mode 100644 index 00000000..284d3a9f --- /dev/null +++ b/src/api/openapi/models/base-response-group-study-members-response-content.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GroupStudyMembersResponseContent } from './group-study-members-response-content'; + +export interface BaseResponseGroupStudyMembersResponseContent { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: GroupStudyMembersResponseContent; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-group-study-progress-grades-response.ts b/src/api/openapi/models/base-response-group-study-progress-grades-response.ts new file mode 100644 index 00000000..d0cbea31 --- /dev/null +++ b/src/api/openapi/models/base-response-group-study-progress-grades-response.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GroupStudyProgressGradesResponse } from './group-study-progress-grades-response'; + +export interface BaseResponseGroupStudyProgressGradesResponse { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: GroupStudyProgressGradesResponse; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-homework-submission-response.ts b/src/api/openapi/models/base-response-homework-submission-response.ts new file mode 100644 index 00000000..4525453e --- /dev/null +++ b/src/api/openapi/models/base-response-homework-submission-response.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { HomeworkSubmissionResponse } from './homework-submission-response'; + +export interface BaseResponseHomeworkSubmissionResponse { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: HomeworkSubmissionResponse; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-matching-request-response.ts b/src/api/openapi/models/base-response-matching-request-response.ts new file mode 100644 index 00000000..80b8d89e --- /dev/null +++ b/src/api/openapi/models/base-response-matching-request-response.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { MatchingRequestResponse } from './matching-request-response'; + +export interface BaseResponseMatchingRequestResponse { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: MatchingRequestResponse; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-member-account-history-response-dto.ts b/src/api/openapi/models/base-response-member-account-history-response-dto.ts new file mode 100644 index 00000000..2a41b5a6 --- /dev/null +++ b/src/api/openapi/models/base-response-member-account-history-response-dto.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { MemberAccountHistoryResponseDto } from './member-account-history-response-dto'; + +export interface BaseResponseMemberAccountHistoryResponseDto { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: MemberAccountHistoryResponseDto; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-object.ts b/src/api/openapi/models/base-response-object.ts new file mode 100644 index 00000000..a3accd0e --- /dev/null +++ b/src/api/openapi/models/base-response-object.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface BaseResponseObject { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: object; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-string.ts b/src/api/openapi/models/base-response-string.ts new file mode 100644 index 00000000..f2d111f8 --- /dev/null +++ b/src/api/openapi/models/base-response-string.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface BaseResponseString { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: string; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-study-space-is-participate-response.ts b/src/api/openapi/models/base-response-study-space-is-participate-response.ts new file mode 100644 index 00000000..21a8a17e --- /dev/null +++ b/src/api/openapi/models/base-response-study-space-is-participate-response.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { StudySpaceIsParticipateResponse } from './study-space-is-participate-response'; + +export interface BaseResponseStudySpaceIsParticipateResponse { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: StudySpaceIsParticipateResponse; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-temporal-token.ts b/src/api/openapi/models/base-response-temporal-token.ts new file mode 100644 index 00000000..3cabcba3 --- /dev/null +++ b/src/api/openapi/models/base-response-temporal-token.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { TemporalToken } from './temporal-token'; + +export interface BaseResponseTemporalToken { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: TemporalToken; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/base-response-void.ts b/src/api/openapi/models/base-response-void.ts new file mode 100644 index 00000000..ee722ea5 --- /dev/null +++ b/src/api/openapi/models/base-response-void.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface BaseResponseVoid { + /** + * Status Code + */ + 'statusCode': number; + /** + * Timestamp + */ + 'timestamp': string; + /** + * Content + */ + 'content'?: object; + /** + * Message + */ + 'message'?: string; +} + diff --git a/src/api/openapi/models/change-hist-dto.ts b/src/api/openapi/models/change-hist-dto.ts new file mode 100644 index 00000000..23c55fb9 --- /dev/null +++ b/src/api/openapi/models/change-hist-dto.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface ChangeHistDto { + 'changedAt'?: string; + 'from'?: string; + 'to'?: string; +} + diff --git a/src/api/openapi/models/cursor-response-dto-study-reservation-member.ts b/src/api/openapi/models/cursor-response-dto-study-reservation-member-dto.ts similarity index 67% rename from src/api/openapi/models/cursor-response-dto-study-reservation-member.ts rename to src/api/openapi/models/cursor-response-dto-study-reservation-member-dto.ts index 42a088d3..c20385a0 100644 --- a/src/api/openapi/models/cursor-response-dto-study-reservation-member.ts +++ b/src/api/openapi/models/cursor-response-dto-study-reservation-member-dto.ts @@ -15,10 +15,10 @@ // May contain unused imports in some cases // @ts-ignore -import type { StudyReservationMember } from './study-reservation-member'; +import type { StudyReservationMemberDto } from './study-reservation-member-dto'; -export interface CursorResponseDtoStudyReservationMember { - 'items'?: Array; +export interface CursorResponseDtoStudyReservationMemberDto { + 'items'?: Array; 'nextCursor'?: number; 'hasNext'?: boolean; } diff --git a/src/api/openapi/models/granted-token-info.ts b/src/api/openapi/models/granted-token-info.ts new file mode 100644 index 00000000..bf11a00d --- /dev/null +++ b/src/api/openapi/models/granted-token-info.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { OAuth2UserInfo } from './oauth2-user-info'; + +export interface GrantedTokenInfo { + 'accessToken': string; + 'refreshToken'?: string; + 'id': string; + 'authVendor': GrantedTokenInfoAuthVendorEnum; + 'name'?: string; + 'profileImageUrl'?: string; + 'userInfo'?: OAuth2UserInfo; +} + +export const GrantedTokenInfoAuthVendorEnum = { + Kakao: 'KAKAO', + Google: 'GOOGLE', + Native: 'NATIVE' +} as const; + +export type GrantedTokenInfoAuthVendorEnum = typeof GrantedTokenInfoAuthVendorEnum[keyof typeof GrantedTokenInfoAuthVendorEnum]; + + diff --git a/src/api/openapi/models/group-study-applicant.ts b/src/api/openapi/models/group-study-applicant.ts index 0dc84eec..d70e741e 100644 --- a/src/api/openapi/models/group-study-applicant.ts +++ b/src/api/openapi/models/group-study-applicant.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; // May contain unused imports in some cases // @ts-ignore import type { SincerityTempResponse } from './sincerity-temp-response'; @@ -35,7 +35,7 @@ export interface GroupStudyApplicant { /** * 신청자 프로필 이미지 */ - 'profileImage'?: Image; + 'profileImage'?: ImageDto; /** * 신청자 한줄 소개 */ diff --git a/src/api/openapi/models/group-study-apply-detail-response.ts b/src/api/openapi/models/group-study-apply-detail-response.ts index 818fdecf..9e740769 100644 --- a/src/api/openapi/models/group-study-apply-detail-response.ts +++ b/src/api/openapi/models/group-study-apply-detail-response.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyApplyResponseContent } from './group-study-apply-response-content'; +import type { GroupStudyApplyResponseDto } from './group-study-apply-response-dto'; export interface GroupStudyApplyDetailResponse { /** @@ -29,7 +29,7 @@ export interface GroupStudyApplyDetailResponse { /** * Content */ - 'content'?: GroupStudyApplyResponseContent; + 'content'?: GroupStudyApplyResponseDto; /** * Message */ diff --git a/src/api/openapi/models/group-study-apply-response-content.ts b/src/api/openapi/models/group-study-apply-response-dto.ts similarity index 70% rename from src/api/openapi/models/group-study-apply-response-content.ts rename to src/api/openapi/models/group-study-apply-response-dto.ts index 4d4ef378..aa883036 100644 --- a/src/api/openapi/models/group-study-apply-response-content.ts +++ b/src/api/openapi/models/group-study-apply-response-dto.ts @@ -20,7 +20,7 @@ import type { InterviewAnswer } from './interview-answer'; /** * 그룹스터디 신청 응답 */ -export interface GroupStudyApplyResponseContent { +export interface GroupStudyApplyResponseDto { /** * 신청 ID */ @@ -40,7 +40,7 @@ export interface GroupStudyApplyResponseContent { /** * 역할 */ - 'role'?: GroupStudyApplyResponseContentRoleEnum; + 'role'?: GroupStudyApplyResponseDtoRoleEnum; /** * 최근 접속일 */ @@ -52,7 +52,7 @@ export interface GroupStudyApplyResponseContent { /** * 신청상태 */ - 'status'?: GroupStudyApplyResponseContentStatusEnum; + 'status'?: GroupStudyApplyResponseDtoStatusEnum; /** * 생성일시 */ @@ -67,13 +67,13 @@ export interface GroupStudyApplyResponseContent { 'deletedAt'?: string; } -export const GroupStudyApplyResponseContentRoleEnum = { +export const GroupStudyApplyResponseDtoRoleEnum = { Participant: 'PARTICIPANT', Leader: 'LEADER' } as const; -export type GroupStudyApplyResponseContentRoleEnum = typeof GroupStudyApplyResponseContentRoleEnum[keyof typeof GroupStudyApplyResponseContentRoleEnum]; -export const GroupStudyApplyResponseContentStatusEnum = { +export type GroupStudyApplyResponseDtoRoleEnum = typeof GroupStudyApplyResponseDtoRoleEnum[keyof typeof GroupStudyApplyResponseDtoRoleEnum]; +export const GroupStudyApplyResponseDtoStatusEnum = { None: 'NONE', Pending: 'PENDING', Approved: 'APPROVED', @@ -82,6 +82,6 @@ export const GroupStudyApplyResponseContentStatusEnum = { Kicked: 'KICKED' } as const; -export type GroupStudyApplyResponseContentStatusEnum = typeof GroupStudyApplyResponseContentStatusEnum[keyof typeof GroupStudyApplyResponseContentStatusEnum]; +export type GroupStudyApplyResponseDtoStatusEnum = typeof GroupStudyApplyResponseDtoStatusEnum[keyof typeof GroupStudyApplyResponseDtoStatusEnum]; diff --git a/src/api/openapi/models/group-study-basic-info-response.ts b/src/api/openapi/models/group-study-basic-info-response-dto.ts similarity index 51% rename from src/api/openapi/models/group-study-basic-info-response.ts rename to src/api/openapi/models/group-study-basic-info-response-dto.ts index e002c13e..59792578 100644 --- a/src/api/openapi/models/group-study-basic-info-response.ts +++ b/src/api/openapi/models/group-study-basic-info-response-dto.ts @@ -15,12 +15,9 @@ // May contain unused imports in some cases // @ts-ignore -import type { StudyReservationMember } from './study-reservation-member'; +import type { StudyReservationMemberDto } from './study-reservation-member-dto'; -/** - * 그룹스터디 기본 정보 응답 - */ -export interface GroupStudyBasicInfoResponse { +export interface GroupStudyBasicInfoResponseDto { /** * 그룹스터디 ID */ @@ -28,23 +25,23 @@ export interface GroupStudyBasicInfoResponse { /** * 스터디 분류 */ - 'classification'?: GroupStudyBasicInfoResponseClassificationEnum; + 'classification'?: GroupStudyBasicInfoResponseDtoClassificationEnum; /** * 그룹스터디 리더 정보 */ - 'leader'?: StudyReservationMember; + 'leader'?: StudyReservationMemberDto; /** * 스터디 타입 */ - 'type'?: GroupStudyBasicInfoResponseTypeEnum; + 'type'?: GroupStudyBasicInfoResponseDtoTypeEnum; /** * 스터디 주최자 구분 */ - 'hostType'?: GroupStudyBasicInfoResponseHostTypeEnum; + 'hostType'?: GroupStudyBasicInfoResponseDtoHostTypeEnum; /** * 스터디 모집 대상 (복수 선택 가능) */ - 'targetRoles'?: Array; + 'targetRoles'?: Array; /** * 스터디 최대 모집인원 */ @@ -68,15 +65,15 @@ export interface GroupStudyBasicInfoResponse { /** * 스터디 경력 레벨 (복수 선택 가능) */ - 'experienceLevels'?: Array; + 'experienceLevels'?: Array; /** * 스터디 진행 방식 */ - 'method'?: GroupStudyBasicInfoResponseMethodEnum; + 'method'?: GroupStudyBasicInfoResponseDtoMethodEnum; /** * 스터디 정기모임 유무 */ - 'regularMeeting'?: GroupStudyBasicInfoResponseRegularMeetingEnum; + 'regularMeeting'?: GroupStudyBasicInfoResponseDtoRegularMeetingEnum; /** * 스터디 진행 장소 (진행방식 입력 이후 받는 데이터) */ @@ -96,7 +93,7 @@ export interface GroupStudyBasicInfoResponse { /** * 스터디 진행상태 */ - 'status'?: GroupStudyBasicInfoResponseStatusEnum; + 'status'?: GroupStudyBasicInfoResponseDtoStatusEnum; /** * 생성일시 */ @@ -111,13 +108,13 @@ export interface GroupStudyBasicInfoResponse { 'deletedAt'?: string; } -export const GroupStudyBasicInfoResponseClassificationEnum = { +export const GroupStudyBasicInfoResponseDtoClassificationEnum = { GroupStudy: 'GROUP_STUDY', PremiumStudy: 'PREMIUM_STUDY' } as const; -export type GroupStudyBasicInfoResponseClassificationEnum = typeof GroupStudyBasicInfoResponseClassificationEnum[keyof typeof GroupStudyBasicInfoResponseClassificationEnum]; -export const GroupStudyBasicInfoResponseTypeEnum = { +export type GroupStudyBasicInfoResponseDtoClassificationEnum = typeof GroupStudyBasicInfoResponseDtoClassificationEnum[keyof typeof GroupStudyBasicInfoResponseDtoClassificationEnum]; +export const GroupStudyBasicInfoResponseDtoTypeEnum = { Project: 'PROJECT', Mentoring: 'MENTORING', Seminar: 'SEMINAR', @@ -126,23 +123,23 @@ export const GroupStudyBasicInfoResponseTypeEnum = { LectureStudy: 'LECTURE_STUDY' } as const; -export type GroupStudyBasicInfoResponseTypeEnum = typeof GroupStudyBasicInfoResponseTypeEnum[keyof typeof GroupStudyBasicInfoResponseTypeEnum]; -export const GroupStudyBasicInfoResponseHostTypeEnum = { +export type GroupStudyBasicInfoResponseDtoTypeEnum = typeof GroupStudyBasicInfoResponseDtoTypeEnum[keyof typeof GroupStudyBasicInfoResponseDtoTypeEnum]; +export const GroupStudyBasicInfoResponseDtoHostTypeEnum = { Zeroone: 'ZEROONE', Mentor: 'MENTOR', General: 'GENERAL' } as const; -export type GroupStudyBasicInfoResponseHostTypeEnum = typeof GroupStudyBasicInfoResponseHostTypeEnum[keyof typeof GroupStudyBasicInfoResponseHostTypeEnum]; -export const GroupStudyBasicInfoResponseTargetRolesEnum = { +export type GroupStudyBasicInfoResponseDtoHostTypeEnum = typeof GroupStudyBasicInfoResponseDtoHostTypeEnum[keyof typeof GroupStudyBasicInfoResponseDtoHostTypeEnum]; +export const GroupStudyBasicInfoResponseDtoTargetRolesEnum = { Backend: 'BACKEND', Frontend: 'FRONTEND', Planner: 'PLANNER', Designer: 'DESIGNER' } as const; -export type GroupStudyBasicInfoResponseTargetRolesEnum = typeof GroupStudyBasicInfoResponseTargetRolesEnum[keyof typeof GroupStudyBasicInfoResponseTargetRolesEnum]; -export const GroupStudyBasicInfoResponseExperienceLevelsEnum = { +export type GroupStudyBasicInfoResponseDtoTargetRolesEnum = typeof GroupStudyBasicInfoResponseDtoTargetRolesEnum[keyof typeof GroupStudyBasicInfoResponseDtoTargetRolesEnum]; +export const GroupStudyBasicInfoResponseDtoExperienceLevelsEnum = { Beginner: 'BEGINNER', JobSeeker: 'JOB_SEEKER', Junior: 'JUNIOR', @@ -150,28 +147,28 @@ export const GroupStudyBasicInfoResponseExperienceLevelsEnum = { Senior: 'SENIOR' } as const; -export type GroupStudyBasicInfoResponseExperienceLevelsEnum = typeof GroupStudyBasicInfoResponseExperienceLevelsEnum[keyof typeof GroupStudyBasicInfoResponseExperienceLevelsEnum]; -export const GroupStudyBasicInfoResponseMethodEnum = { +export type GroupStudyBasicInfoResponseDtoExperienceLevelsEnum = typeof GroupStudyBasicInfoResponseDtoExperienceLevelsEnum[keyof typeof GroupStudyBasicInfoResponseDtoExperienceLevelsEnum]; +export const GroupStudyBasicInfoResponseDtoMethodEnum = { Online: 'ONLINE', Offline: 'OFFLINE', Hybrid: 'HYBRID' } as const; -export type GroupStudyBasicInfoResponseMethodEnum = typeof GroupStudyBasicInfoResponseMethodEnum[keyof typeof GroupStudyBasicInfoResponseMethodEnum]; -export const GroupStudyBasicInfoResponseRegularMeetingEnum = { +export type GroupStudyBasicInfoResponseDtoMethodEnum = typeof GroupStudyBasicInfoResponseDtoMethodEnum[keyof typeof GroupStudyBasicInfoResponseDtoMethodEnum]; +export const GroupStudyBasicInfoResponseDtoRegularMeetingEnum = { None: 'NONE', Weekly: 'WEEKLY', Biweekly: 'BIWEEKLY', TripleWeeklyOrMore: 'TRIPLE_WEEKLY_OR_MORE' } as const; -export type GroupStudyBasicInfoResponseRegularMeetingEnum = typeof GroupStudyBasicInfoResponseRegularMeetingEnum[keyof typeof GroupStudyBasicInfoResponseRegularMeetingEnum]; -export const GroupStudyBasicInfoResponseStatusEnum = { +export type GroupStudyBasicInfoResponseDtoRegularMeetingEnum = typeof GroupStudyBasicInfoResponseDtoRegularMeetingEnum[keyof typeof GroupStudyBasicInfoResponseDtoRegularMeetingEnum]; +export const GroupStudyBasicInfoResponseDtoStatusEnum = { Recruiting: 'RECRUITING', InProgress: 'IN_PROGRESS', Completed: 'COMPLETED' } as const; -export type GroupStudyBasicInfoResponseStatusEnum = typeof GroupStudyBasicInfoResponseStatusEnum[keyof typeof GroupStudyBasicInfoResponseStatusEnum]; +export type GroupStudyBasicInfoResponseDtoStatusEnum = typeof GroupStudyBasicInfoResponseDtoStatusEnum[keyof typeof GroupStudyBasicInfoResponseDtoStatusEnum]; diff --git a/src/api/openapi/models/group-study-creation-response-content.ts b/src/api/openapi/models/group-study-creation-response-dto.ts similarity index 87% rename from src/api/openapi/models/group-study-creation-response-content.ts rename to src/api/openapi/models/group-study-creation-response-dto.ts index 90434cc3..4d89ad08 100644 --- a/src/api/openapi/models/group-study-creation-response-content.ts +++ b/src/api/openapi/models/group-study-creation-response-dto.ts @@ -14,10 +14,7 @@ -/** - * 그룹스터디 생성 응답 - */ -export interface GroupStudyCreationResponseContent { +export interface GroupStudyCreationResponseDto { /** * 생성된 그룹스터디 ID */ diff --git a/src/api/openapi/models/group-study-creation-response.ts b/src/api/openapi/models/group-study-creation-response.ts index 0b7724de..5cfddc5f 100644 --- a/src/api/openapi/models/group-study-creation-response.ts +++ b/src/api/openapi/models/group-study-creation-response.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyCreationResponseContent } from './group-study-creation-response-content'; +import type { GroupStudyCreationResponseDto } from './group-study-creation-response-dto'; export interface GroupStudyCreationResponse { /** @@ -29,7 +29,7 @@ export interface GroupStudyCreationResponse { /** * Content */ - 'content'?: GroupStudyCreationResponseContent; + 'content'?: GroupStudyCreationResponseDto; /** * Message */ diff --git a/src/api/openapi/models/group-study-detail-info-response.ts b/src/api/openapi/models/group-study-detail-info-response-dto.ts similarity index 85% rename from src/api/openapi/models/group-study-detail-info-response.ts rename to src/api/openapi/models/group-study-detail-info-response-dto.ts index 892d536a..7a2b6a14 100644 --- a/src/api/openapi/models/group-study-detail-info-response.ts +++ b/src/api/openapi/models/group-study-detail-info-response-dto.ts @@ -15,16 +15,13 @@ // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; -/** - * 그룹스터디 상세 정보 응답 - */ -export interface GroupStudyDetailInfoResponse { +export interface GroupStudyDetailInfoResponseDto { /** * 기존 스터디 썸네일 이미지 */ - 'image'?: Image; + 'image'?: ImageDto; /** * 스터디 제목 */ diff --git a/src/api/openapi/models/group-study-detail-response.ts b/src/api/openapi/models/group-study-detail-response.ts index 0f6b1df6..797235db 100644 --- a/src/api/openapi/models/group-study-detail-response.ts +++ b/src/api/openapi/models/group-study-detail-response.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyDetailResponseContent } from './group-study-detail-response-content'; +import type { GroupStudyFullResponseDto } from './group-study-full-response-dto'; export interface GroupStudyDetailResponse { /** @@ -29,7 +29,7 @@ export interface GroupStudyDetailResponse { /** * Content */ - 'content'?: GroupStudyDetailResponseContent; + 'content'?: GroupStudyFullResponseDto; /** * Message */ diff --git a/src/api/openapi/models/group-study-detail-response-content.ts b/src/api/openapi/models/group-study-full-response-dto.ts similarity index 53% rename from src/api/openapi/models/group-study-detail-response-content.ts rename to src/api/openapi/models/group-study-full-response-dto.ts index d91856b1..ac142d46 100644 --- a/src/api/openapi/models/group-study-detail-response-content.ts +++ b/src/api/openapi/models/group-study-full-response-dto.ts @@ -15,29 +15,26 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudInterviewPostResponseContent } from './group-stud-interview-post-response-content'; +import type { GroupStudyBasicInfoResponseDto } from './group-study-basic-info-response-dto'; // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyBasicInfoResponse } from './group-study-basic-info-response'; +import type { GroupStudyDetailInfoResponseDto } from './group-study-detail-info-response-dto'; // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyDetailInfoResponse } from './group-study-detail-info-response'; +import type { GroupStudyInterviewPostResponseDto } from './group-study-interview-post-response-dto'; -/** - * 그룹스터디 상세 조회 응답 - */ -export interface GroupStudyDetailResponseContent { +export interface GroupStudyFullResponseDto { /** * 그룹스터디 기본 정보 */ - 'basicInfo'?: GroupStudyBasicInfoResponse; + 'basicInfo'?: GroupStudyBasicInfoResponseDto; /** * 그룹스터디 상세 정보 */ - 'detailInfo'?: GroupStudyDetailInfoResponse; + 'detailInfo'?: GroupStudyDetailInfoResponseDto; /** * 그룹스터디 면접 질문 */ - 'interviewPost'?: GroupStudInterviewPostResponseContent; + 'interviewPost'?: GroupStudyInterviewPostResponseDto; } diff --git a/src/api/openapi/models/group-stud-interview-post-response-content.ts b/src/api/openapi/models/group-study-interview-post-response-dto.ts similarity index 93% rename from src/api/openapi/models/group-stud-interview-post-response-content.ts rename to src/api/openapi/models/group-study-interview-post-response-dto.ts index 2a0eb04d..f9856978 100644 --- a/src/api/openapi/models/group-stud-interview-post-response-content.ts +++ b/src/api/openapi/models/group-study-interview-post-response-dto.ts @@ -20,7 +20,7 @@ import type { InterviewQuestion } from './interview-question'; /** * 그룹스터디 개설질문 응답 */ -export interface GroupStudInterviewPostResponseContent { +export interface GroupStudyInterviewPostResponseDto { /** * 스터디 리더가 작성한 개설질문 목록 (최대 10개) */ diff --git a/src/api/openapi/models/group-study-interview-post-update-response.ts b/src/api/openapi/models/group-study-interview-post-update-response.ts index 919ca0d6..d8e0c6ca 100644 --- a/src/api/openapi/models/group-study-interview-post-update-response.ts +++ b/src/api/openapi/models/group-study-interview-post-update-response.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudInterviewPostResponseContent } from './group-stud-interview-post-response-content'; +import type { GroupStudyInterviewPostResponseDto } from './group-study-interview-post-response-dto'; export interface GroupStudyInterviewPostUpdateResponse { /** @@ -29,7 +29,7 @@ export interface GroupStudyInterviewPostUpdateResponse { /** * Content */ - 'content'?: GroupStudInterviewPostResponseContent; + 'content'?: GroupStudyInterviewPostResponseDto; /** * Message */ diff --git a/src/api/openapi/models/group-study-list-item.ts b/src/api/openapi/models/group-study-list-item-dto.ts similarity index 63% rename from src/api/openapi/models/group-study-list-item.ts rename to src/api/openapi/models/group-study-list-item-dto.ts index f82bda26..e413d804 100644 --- a/src/api/openapi/models/group-study-list-item.ts +++ b/src/api/openapi/models/group-study-list-item-dto.ts @@ -15,22 +15,22 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyBasicInfoResponse } from './group-study-basic-info-response'; +import type { GroupStudyBasicInfoResponseDto } from './group-study-basic-info-response-dto'; // May contain unused imports in some cases // @ts-ignore -import type { GroupStudySimpleInfoResponse } from './group-study-simple-info-response'; +import type { GroupStudySimpleInfoResponseDto } from './group-study-simple-info-response-dto'; /** * 그룹스터디 목록 아이템 (PageResponse에서 사용) */ -export interface GroupStudyListItem { +export interface GroupStudyListItemDto { /** * 그룹스터디 기본 정보 */ - 'basicInfo'?: GroupStudyBasicInfoResponse; + 'basicInfo'?: GroupStudyBasicInfoResponseDto; /** * 그룹스터디 간단한 상세 정보 */ - 'simpleDetailInfo'?: GroupStudySimpleInfoResponse; + 'simpleDetailInfo'?: GroupStudySimpleInfoResponseDto; } diff --git a/src/api/openapi/models/group-study-list-response.ts b/src/api/openapi/models/group-study-list-response.ts index b3f20830..819e2dd0 100644 --- a/src/api/openapi/models/group-study-list-response.ts +++ b/src/api/openapi/models/group-study-list-response.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { PageResponseGroupStudyListItem } from './page-response-group-study-list-item'; +import type { PageResponseGroupStudyListItemDto } from './page-response-group-study-list-item-dto'; export interface GroupStudyListResponse { /** @@ -29,7 +29,7 @@ export interface GroupStudyListResponse { /** * Content */ - 'content'?: PageResponseGroupStudyListItem; + 'content'?: PageResponseGroupStudyListItemDto; /** * Message */ diff --git a/src/api/openapi/models/group-study-progress-grade-response.ts b/src/api/openapi/models/group-study-progress-grade-response.ts new file mode 100644 index 00000000..326d66d6 --- /dev/null +++ b/src/api/openapi/models/group-study-progress-grade-response.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface GroupStudyProgressGradeResponse { + 'id'?: number; + 'code'?: string; + 'name'?: string; + 'score'?: number; +} + diff --git a/src/api/openapi/models/group-study-progress-grades-response.ts b/src/api/openapi/models/group-study-progress-grades-response.ts new file mode 100644 index 00000000..a4c34423 --- /dev/null +++ b/src/api/openapi/models/group-study-progress-grades-response.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { GroupStudyProgressGradeResponse } from './group-study-progress-grade-response'; + +export interface GroupStudyProgressGradesResponse { + 'grades'?: Array; +} + diff --git a/src/api/openapi/models/group-study-simple-info-response.ts b/src/api/openapi/models/group-study-simple-info-response-dto.ts similarity index 83% rename from src/api/openapi/models/group-study-simple-info-response.ts rename to src/api/openapi/models/group-study-simple-info-response-dto.ts index 7dbf5779..36c9ca7f 100644 --- a/src/api/openapi/models/group-study-simple-info-response.ts +++ b/src/api/openapi/models/group-study-simple-info-response-dto.ts @@ -15,16 +15,16 @@ // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; /** * 그룹스터디 간단한 상세 정보 (목록조회용) */ -export interface GroupStudySimpleInfoResponse { +export interface GroupStudySimpleInfoResponseDto { /** * 스터디 썸네일 이미지 */ - 'thumbnail'?: Image; + 'thumbnail'?: ImageDto; /** * 스터디 제목 */ diff --git a/src/api/openapi/models/homework-submission-response.ts b/src/api/openapi/models/homework-submission-response.ts new file mode 100644 index 00000000..58568bae --- /dev/null +++ b/src/api/openapi/models/homework-submission-response.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface HomeworkSubmissionResponse { + 'homeworkId'?: number; +} + diff --git a/src/api/openapi/models/image.ts b/src/api/openapi/models/image-dto.ts similarity index 79% rename from src/api/openapi/models/image.ts rename to src/api/openapi/models/image-dto.ts index e83afd1e..bf572585 100644 --- a/src/api/openapi/models/image.ts +++ b/src/api/openapi/models/image-dto.ts @@ -15,9 +15,9 @@ // May contain unused imports in some cases // @ts-ignore -import type { ResizedImage } from './resized-image'; +import type { ResizedImageDto } from './resized-image-dto'; -export interface Image { +export interface ImageDto { /** * 이미지 ID */ @@ -25,6 +25,6 @@ export interface Image { /** * 같은 이미지를 여러 사이즈로 리사이징하여 생성된 이미지 목록 */ - 'resizedImages'?: Array; + 'resizedImages'?: Array; } diff --git a/src/api/openapi/models/index.ts b/src/api/openapi/models/index.ts index a597f712..4b9eba37 100644 --- a/src/api/openapi/models/index.ts +++ b/src/api/openapi/models/index.ts @@ -12,15 +12,27 @@ export * from './available-study-time-list-response'; export * from './bank-list-response-schema'; export * from './bank-response'; export * from './base-response'; +export * from './base-response-granted-token-info'; +export * from './base-response-group-study-members-response-content'; +export * from './base-response-group-study-progress-grades-response'; +export * from './base-response-homework-submission-response'; +export * from './base-response-matching-request-response'; +export * from './base-response-member-account-history-response-dto'; +export * from './base-response-object'; +export * from './base-response-string'; +export * from './base-response-study-space-is-participate-response'; +export * from './base-response-temporal-token'; +export * from './base-response-void'; export * from './calendar-day-status'; export * from './career-list-response'; export * from './career-response-dto'; +export * from './change-hist-dto'; export * from './complete-daily-study-request'; export * from './complete-daily-study-request-schema'; export * from './create-mission-response-schema'; export * from './create-study-space-request'; export * from './cursor-response-dto-daily-study-response'; -export * from './cursor-response-dto-study-reservation-member'; +export * from './cursor-response-dto-study-reservation-member-dto'; export * from './daily-study-response'; export * from './error-response'; export * from './evaluation-request'; @@ -35,7 +47,7 @@ export * from './get-study-dashboard-response-schema'; export * from './get-study-space-response-schema'; export * from './get-study-spaces-response-schema'; export * from './get-today-my-daily-study-response-schema'; -export * from './group-stud-interview-post-response-content'; +export * from './granted-token-info'; export * from './group-study-applicant'; export * from './group-study-apply-creation-response'; export * from './group-study-apply-creation-response-content'; @@ -46,22 +58,23 @@ export * from './group-study-apply-process-request'; export * from './group-study-apply-process-response'; export * from './group-study-apply-process-response-content'; export * from './group-study-apply-request'; -export * from './group-study-apply-response-content'; +export * from './group-study-apply-response-dto'; export * from './group-study-apply-update-request'; export * from './group-study-apply-update-response-content'; export * from './group-study-basic-info-request-dto'; -export * from './group-study-basic-info-response'; +export * from './group-study-basic-info-response-dto'; export * from './group-study-basic-info-update-request-dto'; export * from './group-study-creation-request-dto'; export * from './group-study-creation-response'; -export * from './group-study-creation-response-content'; +export * from './group-study-creation-response-dto'; export * from './group-study-detail-info-request-dto'; -export * from './group-study-detail-info-response'; +export * from './group-study-detail-info-response-dto'; export * from './group-study-detail-response'; -export * from './group-study-detail-response-content'; +export * from './group-study-full-response-dto'; export * from './group-study-interview-post-request'; +export * from './group-study-interview-post-response-dto'; export * from './group-study-interview-post-update-response'; -export * from './group-study-list-item'; +export * from './group-study-list-item-dto'; export * from './group-study-list-response'; export * from './group-study-member-progress-grade-response'; export * from './group-study-member-progress-history-response'; @@ -72,7 +85,9 @@ export * from './group-study-members-response-content'; export * from './group-study-notice-request'; export * from './group-study-notice-response'; export * from './group-study-notice-response-content'; -export * from './group-study-simple-info-response'; +export * from './group-study-progress-grade-response'; +export * from './group-study-progress-grades-response'; +export * from './group-study-simple-info-response-dto'; export * from './group-study-thread-comment-request'; export * from './group-study-thread-request'; export * from './group-study-update-request'; @@ -82,8 +97,9 @@ export * from './has-member-new-notification-response'; export * from './has-member-new-notification-response-schema'; export * from './homework-edit-request'; export * from './homework-submission-request'; +export * from './homework-submission-response'; export * from './id-name-dto'; -export * from './image'; +export * from './image-dto'; export * from './image-size-type'; export * from './interview-answer'; export * from './interview-question'; @@ -91,8 +107,10 @@ export * from './job-list-response'; export * from './job-response-dto'; export * from './local-time'; export * from './matching-attributes'; +export * from './matching-request-response'; export * from './matching-system-status-response'; export * from './matching-system-status-schema'; +export * from './member-account-history-response-dto'; export * from './member-creation-request'; export * from './member-creation-response'; export * from './member-creation-response-content'; @@ -115,7 +133,7 @@ export * from './mission-task-dto'; export * from './mission-update-request'; export * from './monthly-calendar-response'; export * from './my-group-study-application-response'; -export * from './my-group-study-apply-list-item'; +export * from './my-group-study-apply-list-item-dto'; export * from './nickname-availability-response'; export * from './nickname-availability-response-dto'; export * from './no-content-response'; @@ -123,14 +141,15 @@ export * from './notification-categories-response'; export * from './notification-categories-response-schema'; export * from './notification-topic-response'; export * from './number-response'; +export * from './oauth2-user-info'; export * from './optional-homework-submission'; export * from './page-admin-transaction-list-response-schema'; export * from './page-response-admin-transaction-list-response'; export * from './page-response-group-study-apply-list-item'; -export * from './page-response-group-study-list-item'; +export * from './page-response-group-study-list-item-dto'; export * from './page-response-member-notification-response'; export * from './page-response-mission-list-response'; -export * from './page-response-my-group-study-apply-list-item'; +export * from './page-response-my-group-study-apply-list-item-dto'; export * from './page-response-participating-study-info'; export * from './page-response-study-refund-summary-response'; export * from './page-response-study-settlement-summary-response'; @@ -162,7 +181,7 @@ export * from './request-auto-study-matching-dto'; export * from './reset-weekly-matching-request'; export * from './reset-weekly-matching-response'; export * from './reset-weekly-matching-schema'; -export * from './resized-image'; +export * from './resized-image-dto'; export * from './settlement-account-register-request'; export * from './settlement-account-response'; export * from './settlement-account-response-schema'; @@ -185,7 +204,7 @@ export * from './study-refund-detail-response'; export * from './study-refund-detail-response-schema'; export * from './study-refund-reject-request'; export * from './study-refund-summary-response'; -export * from './study-reservation-member'; +export * from './study-reservation-member-dto'; export * from './study-reservation-response'; export * from './study-reservation-response-content'; export * from './study-settlement-create-request'; @@ -197,6 +216,7 @@ export * from './study-space-is-participate-response-schema'; export * from './study-subject-dto'; export * from './study-subject-list-response'; export * from './tech-stack-response'; +export * from './temporal-token'; export * from './thread-comment-response-content'; export * from './thread-comment-response-schema'; export * from './thread-summary-response'; diff --git a/src/api/openapi/models/matching-request-response.ts b/src/api/openapi/models/matching-request-response.ts new file mode 100644 index 00000000..ff7ba55e --- /dev/null +++ b/src/api/openapi/models/matching-request-response.ts @@ -0,0 +1,70 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface MatchingRequestResponse { + /** + * 매칭 요청 ID + */ + 'matchingRequestId'?: number; + /** + * 요청 회원 ID + */ + 'memberId'?: number; + /** + * 파트너 회원 ID + */ + 'partnerId'?: number; + /** + * 매칭 상태 + */ + 'status'?: MatchingRequestResponseStatusEnum; + /** + * 매칭 종류 + */ + 'type'?: MatchingRequestResponseTypeEnum; + /** + * 내용/메모 + */ + 'content'?: string; + /** + * 생성 일시 + */ + 'createdAt'?: string; + /** + * 수정 일시 + */ + 'updatedAt'?: string; +} + +export const MatchingRequestResponseStatusEnum = { + Pending: 'PENDING', + ResAcpt: 'RES_ACPT', + ResAuto: 'RES_AUTO', + ResRej: 'RES_REJ', + Auto: 'AUTO', + Done: 'DONE', + Cancel: 'CANCEL' +} as const; + +export type MatchingRequestResponseStatusEnum = typeof MatchingRequestResponseStatusEnum[keyof typeof MatchingRequestResponseStatusEnum]; +export const MatchingRequestResponseTypeEnum = { + Auto: 'AUTO', + Manual: 'MANUAL' +} as const; + +export type MatchingRequestResponseTypeEnum = typeof MatchingRequestResponseTypeEnum[keyof typeof MatchingRequestResponseTypeEnum]; + + diff --git a/src/api/openapi/models/member-account-history-response-dto.ts b/src/api/openapi/models/member-account-history-response-dto.ts new file mode 100644 index 00000000..88da27bd --- /dev/null +++ b/src/api/openapi/models/member-account-history-response-dto.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ChangeHistDto } from './change-hist-dto'; + +export interface MemberAccountHistoryResponseDto { + 'memberId'?: number; + 'joinedAt'?: string; + 'loginMostRecentlyAt'?: string; + 'loginHists'?: Array; + 'roleChangeHists'?: Array; + 'memberStatusChangeHists'?: Array; +} + diff --git a/src/api/openapi/models/member-profile-response-dto.ts b/src/api/openapi/models/member-profile-response-dto.ts index 62535239..51436db9 100644 --- a/src/api/openapi/models/member-profile-response-dto.ts +++ b/src/api/openapi/models/member-profile-response-dto.ts @@ -18,7 +18,7 @@ import type { IdNameDto } from './id-name-dto'; // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; // May contain unused imports in some cases // @ts-ignore import type { SocialMediaResponseDto } from './social-media-response-dto'; @@ -38,7 +38,7 @@ export interface MemberProfileResponseDto { /** * 프로필 이미지 - 리사이징된 이미지를 포함하고 있음 - 지금은 ORIGINAL 하나밖에 없음 */ - 'profileImage'?: Image; + 'profileImage'?: ImageDto; /** * 한마디 소개 */ diff --git a/src/api/openapi/models/my-group-study-application-response.ts b/src/api/openapi/models/my-group-study-application-response.ts index 696e495d..51b1bf27 100644 --- a/src/api/openapi/models/my-group-study-application-response.ts +++ b/src/api/openapi/models/my-group-study-application-response.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { PageResponseMyGroupStudyApplyListItem } from './page-response-my-group-study-apply-list-item'; +import type { PageResponseMyGroupStudyApplyListItemDto } from './page-response-my-group-study-apply-list-item-dto'; export interface MyGroupStudyApplicationResponse { /** @@ -29,7 +29,7 @@ export interface MyGroupStudyApplicationResponse { /** * Content */ - 'content'?: PageResponseMyGroupStudyApplyListItem; + 'content'?: PageResponseMyGroupStudyApplyListItemDto; /** * Message */ diff --git a/src/api/openapi/models/my-group-study-apply-list-item.ts b/src/api/openapi/models/my-group-study-apply-list-item-dto.ts similarity index 62% rename from src/api/openapi/models/my-group-study-apply-list-item.ts rename to src/api/openapi/models/my-group-study-apply-list-item-dto.ts index 83a62b08..163023e0 100644 --- a/src/api/openapi/models/my-group-study-apply-list-item.ts +++ b/src/api/openapi/models/my-group-study-apply-list-item-dto.ts @@ -15,30 +15,30 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyApplyResponseContent } from './group-study-apply-response-content'; +import type { GroupStudyApplyResponseDto } from './group-study-apply-response-dto'; // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyBasicInfoResponse } from './group-study-basic-info-response'; +import type { GroupStudyBasicInfoResponseDto } from './group-study-basic-info-response-dto'; // May contain unused imports in some cases // @ts-ignore -import type { GroupStudySimpleInfoResponse } from './group-study-simple-info-response'; +import type { GroupStudySimpleInfoResponseDto } from './group-study-simple-info-response-dto'; /** * 내가 신청한 그룹스터디 목록 아이템 (PageResponse에서 사용) */ -export interface MyGroupStudyApplyListItem { +export interface MyGroupStudyApplyListItemDto { /** * 그룹스터디 기본 정보 */ - 'basicInfo'?: GroupStudyBasicInfoResponse; + 'basicInfo'?: GroupStudyBasicInfoResponseDto; /** * 그룹스터디 간단한 상세 정보 */ - 'simpleDetailInfo'?: GroupStudySimpleInfoResponse; + 'simpleDetailInfo'?: GroupStudySimpleInfoResponseDto; /** * 그룹스터디 신청 정보 */ - 'applyInfo'?: GroupStudyApplyResponseContent; + 'applyInfo'?: GroupStudyApplyResponseDto; /** * 후기 작성 여부 */ diff --git a/src/api/openapi/models/oauth2-user-info.ts b/src/api/openapi/models/oauth2-user-info.ts new file mode 100644 index 00000000..041d25b7 --- /dev/null +++ b/src/api/openapi/models/oauth2-user-info.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface OAuth2UserInfo { + 'id'?: string; + 'name'?: string; + 'profileImageUrl'?: string; +} + diff --git a/src/api/openapi/models/page-response-group-study-list-item.ts b/src/api/openapi/models/page-response-group-study-list-item-dto.ts similarity index 74% rename from src/api/openapi/models/page-response-group-study-list-item.ts rename to src/api/openapi/models/page-response-group-study-list-item-dto.ts index abc656fa..a31b52de 100644 --- a/src/api/openapi/models/page-response-group-study-list-item.ts +++ b/src/api/openapi/models/page-response-group-study-list-item-dto.ts @@ -15,10 +15,10 @@ // May contain unused imports in some cases // @ts-ignore -import type { GroupStudyListItem } from './group-study-list-item'; +import type { GroupStudyListItemDto } from './group-study-list-item-dto'; -export interface PageResponseGroupStudyListItem { - 'content'?: Array; +export interface PageResponseGroupStudyListItemDto { + 'content'?: Array; 'page'?: number; 'size'?: number; 'totalElements'?: number; diff --git a/src/api/openapi/models/page-response-my-group-study-apply-list-item.ts b/src/api/openapi/models/page-response-my-group-study-apply-list-item-dto.ts similarity index 71% rename from src/api/openapi/models/page-response-my-group-study-apply-list-item.ts rename to src/api/openapi/models/page-response-my-group-study-apply-list-item-dto.ts index ebba4169..6011d1b2 100644 --- a/src/api/openapi/models/page-response-my-group-study-apply-list-item.ts +++ b/src/api/openapi/models/page-response-my-group-study-apply-list-item-dto.ts @@ -15,10 +15,10 @@ // May contain unused imports in some cases // @ts-ignore -import type { MyGroupStudyApplyListItem } from './my-group-study-apply-list-item'; +import type { MyGroupStudyApplyListItemDto } from './my-group-study-apply-list-item-dto'; -export interface PageResponseMyGroupStudyApplyListItem { - 'content'?: Array; +export interface PageResponseMyGroupStudyApplyListItemDto { + 'content'?: Array; 'page'?: number; 'size'?: number; 'totalElements'?: number; diff --git a/src/api/openapi/models/participating-study-info.ts b/src/api/openapi/models/participating-study-info.ts index 75e3168e..4603942e 100644 --- a/src/api/openapi/models/participating-study-info.ts +++ b/src/api/openapi/models/participating-study-info.ts @@ -15,7 +15,7 @@ // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; export interface ParticipatingStudyInfo { /** @@ -25,7 +25,7 @@ export interface ParticipatingStudyInfo { /** * 섬네일 이미지 */ - 'thumbnail'?: Image; + 'thumbnail'?: ImageDto; /** * 스터디 제목 - 일대일 스터디인 경우 null */ diff --git a/src/api/openapi/models/resized-image.ts b/src/api/openapi/models/resized-image-dto.ts similarity index 94% rename from src/api/openapi/models/resized-image.ts rename to src/api/openapi/models/resized-image-dto.ts index 8975985b..0b9fa24d 100644 --- a/src/api/openapi/models/resized-image.ts +++ b/src/api/openapi/models/resized-image-dto.ts @@ -17,7 +17,7 @@ // @ts-ignore import type { ImageSizeType } from './image-size-type'; -export interface ResizedImage { +export interface ResizedImageDto { /** * 리사이징 이미지 ID */ diff --git a/src/api/openapi/models/study-reservation-member.ts b/src/api/openapi/models/study-reservation-member-dto.ts similarity index 78% rename from src/api/openapi/models/study-reservation-member.ts rename to src/api/openapi/models/study-reservation-member-dto.ts index 386044d6..14a2e701 100644 --- a/src/api/openapi/models/study-reservation-member.ts +++ b/src/api/openapi/models/study-reservation-member-dto.ts @@ -15,12 +15,12 @@ // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; -export interface StudyReservationMember { +export interface StudyReservationMemberDto { 'memberId'?: number; 'memberNickname'?: string; - 'profileImage'?: Image; + 'profileImage'?: ImageDto; 'simpleIntroduction'?: string; } diff --git a/src/api/openapi/models/study-reservation-response-content.ts b/src/api/openapi/models/study-reservation-response-content.ts index f74bec65..4b2b3b11 100644 --- a/src/api/openapi/models/study-reservation-response-content.ts +++ b/src/api/openapi/models/study-reservation-response-content.ts @@ -15,10 +15,10 @@ // May contain unused imports in some cases // @ts-ignore -import type { CursorResponseDtoStudyReservationMember } from './cursor-response-dto-study-reservation-member'; +import type { CursorResponseDtoStudyReservationMemberDto } from './cursor-response-dto-study-reservation-member-dto'; export interface StudyReservationResponseContent { - 'members'?: CursorResponseDtoStudyReservationMember; + 'members'?: CursorResponseDtoStudyReservationMemberDto; 'totalMemberCount'?: number; } diff --git a/src/api/openapi/models/temporal-token.ts b/src/api/openapi/models/temporal-token.ts new file mode 100644 index 00000000..1865b584 --- /dev/null +++ b/src/api/openapi/models/temporal-token.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface TemporalToken { + 'accessToken'?: string; +} + diff --git a/src/api/openapi/models/thread-comment-response-content.ts b/src/api/openapi/models/thread-comment-response-content.ts index 1b8a7676..a3956761 100644 --- a/src/api/openapi/models/thread-comment-response-content.ts +++ b/src/api/openapi/models/thread-comment-response-content.ts @@ -15,14 +15,14 @@ // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; export interface ThreadCommentResponseContent { 'commentId'?: number; 'threadId'?: number; 'authorId'?: number; 'authorName'?: string; - 'image'?: Image; + 'image'?: ImageDto; 'content'?: string; 'isLeader'?: boolean; 'likesCount'?: number; diff --git a/src/api/openapi/models/thread-summary-response-content.ts b/src/api/openapi/models/thread-summary-response-content.ts index 43b4f4bd..a7f72aac 100644 --- a/src/api/openapi/models/thread-summary-response-content.ts +++ b/src/api/openapi/models/thread-summary-response-content.ts @@ -15,14 +15,14 @@ // May contain unused imports in some cases // @ts-ignore -import type { Image } from './image'; +import type { ImageDto } from './image-dto'; export interface ThreadSummaryResponseContent { 'threadId'?: number; 'groupStudyId'?: number; 'authorId'?: number; 'authorName'?: string; - 'image'?: Image; + 'image'?: ImageDto; 'content'?: string; 'isLeader'?: boolean; 'likesCount'?: number; From 8e33bbec9377a0c433e0245d8dd5b2cdb8ea1f74 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 29 Dec 2025 12:13:00 +0000 Subject: [PATCH 029/211] feat: new OpenAPI (automated) --- src/api/openapi/.openapi-generator/FILES | 8 ++ src/api/openapi/api.ts | 1 + src/api/openapi/api/toss-webhook-api.ts | 132 ++++++++++++++++++ src/api/openapi/docs/CancelData.md | 30 ++++ src/api/openapi/docs/PaymentData.md | 36 +++++ src/api/openapi/docs/TossWebhookApi.md | 60 ++++++++ src/api/openapi/docs/TossWebhookPayload.md | 28 ++++ src/api/openapi/models/cancel-data.ts | 25 ++++ src/api/openapi/models/index.ts | 3 + src/api/openapi/models/payment-data.ts | 31 ++++ .../openapi/models/toss-webhook-payload.ts | 30 ++++ 11 files changed, 384 insertions(+) create mode 100644 src/api/openapi/api/toss-webhook-api.ts create mode 100644 src/api/openapi/docs/CancelData.md create mode 100644 src/api/openapi/docs/PaymentData.md create mode 100644 src/api/openapi/docs/TossWebhookApi.md create mode 100644 src/api/openapi/docs/TossWebhookPayload.md create mode 100644 src/api/openapi/models/cancel-data.ts create mode 100644 src/api/openapi/models/payment-data.ts create mode 100644 src/api/openapi/models/toss-webhook-payload.ts diff --git a/src/api/openapi/.openapi-generator/FILES b/src/api/openapi/.openapi-generator/FILES index d461ca76..6bffe034 100644 --- a/src/api/openapi/.openapi-generator/FILES +++ b/src/api/openapi/.openapi-generator/FILES @@ -36,6 +36,7 @@ api/study-review-api.ts api/study-space-api.ts api/tech-stack-api.ts api/token-apiapi.ts +api/toss-webhook-api.ts base.ts common.ts configuration.ts @@ -73,6 +74,7 @@ docs/BaseResponseStudySpaceIsParticipateResponse.md docs/BaseResponseTemporalToken.md docs/BaseResponseVoid.md docs/CalendarDayStatus.md +docs/CancelData.md docs/CareerListResponse.md docs/CareerResponseDto.md docs/ChangeHistDto.md @@ -228,6 +230,7 @@ docs/Pageable.md docs/ParticipatingStudyInfo.md docs/ParticipatingStudyResponse.md docs/ParticipatingStudyResponseContent.md +docs/PaymentData.md docs/PaymentHistoryResponse.md docs/PaymentSearchCondition.md docs/PaymentUserApi.md @@ -298,6 +301,8 @@ docs/ThreadSummaryResponseContent.md docs/TodayStudyDataResponse.md docs/TokenAPIApi.md docs/TossPaymentConfirmRequest.md +docs/TossWebhookApi.md +docs/TossWebhookPayload.md docs/UpdateGroupStudyMemberGreetingRequest.md docs/UpdateGroupStudyMemberProgressRequest.md docs/UpdateMissionResponseSchema.md @@ -334,6 +339,7 @@ models/base-response-temporal-token.ts models/base-response-void.ts models/base-response.ts models/calendar-day-status.ts +models/cancel-data.ts models/career-list-response.ts models/career-response-dto.ts models/change-hist-dto.ts @@ -474,6 +480,7 @@ models/pageable.ts models/participating-study-info.ts models/participating-study-response-content.ts models/participating-study-response.ts +models/payment-data.ts models/payment-history-response.ts models/payment-search-condition.ts models/phone-auth-response-dto.ts @@ -534,6 +541,7 @@ models/thread-summary-response-content.ts models/thread-summary-response.ts models/today-study-data-response.ts models/toss-payment-confirm-request.ts +models/toss-webhook-payload.ts models/update-group-study-member-greeting-request.ts models/update-group-study-member-progress-request.ts models/update-mission-response-schema.ts diff --git a/src/api/openapi/api.ts b/src/api/openapi/api.ts index 8aa8088f..5c79f912 100644 --- a/src/api/openapi/api.ts +++ b/src/api/openapi/api.ts @@ -48,4 +48,5 @@ export * from './api/study-review-api'; export * from './api/study-space-api'; export * from './api/tech-stack-api'; export * from './api/token-apiapi'; +export * from './api/toss-webhook-api'; diff --git a/src/api/openapi/api/toss-webhook-api.ts b/src/api/openapi/api/toss-webhook-api.ts new file mode 100644 index 00000000..dfcb591d --- /dev/null +++ b/src/api/openapi/api/toss-webhook-api.ts @@ -0,0 +1,132 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { BaseResponseVoid } from '../models'; +// @ts-ignore +import type { TossWebhookPayload } from '../models'; +/** + * TossWebhookApi - axios parameter creator + */ +export const TossWebhookApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * Toss Payments에서 발생한 이벤트를 수신합니다. secret 값으로 검증합니다. + * @summary Toss Webhook 수신 + * @param {TossWebhookPayload} tossWebhookPayload + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + handleTossWebhook: async (tossWebhookPayload: TossWebhookPayload, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'tossWebhookPayload' is not null or undefined + assertParamExists('handleTossWebhook', 'tossWebhookPayload', tossWebhookPayload) + const localVarPath = `/api/v1/webhooks/toss`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(tossWebhookPayload, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TossWebhookApi - functional programming interface + */ +export const TossWebhookApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TossWebhookApiAxiosParamCreator(configuration) + return { + /** + * Toss Payments에서 발생한 이벤트를 수신합니다. secret 값으로 검증합니다. + * @summary Toss Webhook 수신 + * @param {TossWebhookPayload} tossWebhookPayload + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async handleTossWebhook(tossWebhookPayload: TossWebhookPayload, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.handleTossWebhook(tossWebhookPayload, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['TossWebhookApi.handleTossWebhook']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * TossWebhookApi - factory interface + */ +export const TossWebhookApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TossWebhookApiFp(configuration) + return { + /** + * Toss Payments에서 발생한 이벤트를 수신합니다. secret 값으로 검증합니다. + * @summary Toss Webhook 수신 + * @param {TossWebhookPayload} tossWebhookPayload + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + handleTossWebhook(tossWebhookPayload: TossWebhookPayload, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.handleTossWebhook(tossWebhookPayload, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TossWebhookApi - object-oriented interface + */ +export class TossWebhookApi extends BaseAPI { + /** + * Toss Payments에서 발생한 이벤트를 수신합니다. secret 값으로 검증합니다. + * @summary Toss Webhook 수신 + * @param {TossWebhookPayload} tossWebhookPayload + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public handleTossWebhook(tossWebhookPayload: TossWebhookPayload, options?: RawAxiosRequestConfig) { + return TossWebhookApiFp(this.configuration).handleTossWebhook(tossWebhookPayload, options).then((request) => request(this.axios, this.basePath)); + } +} + diff --git a/src/api/openapi/docs/CancelData.md b/src/api/openapi/docs/CancelData.md new file mode 100644 index 00000000..901c54c3 --- /dev/null +++ b/src/api/openapi/docs/CancelData.md @@ -0,0 +1,30 @@ +# CancelData + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**transactionKey** | **string** | | [optional] [default to undefined] +**cancelReason** | **string** | | [optional] [default to undefined] +**cancelAmount** | **number** | | [optional] [default to undefined] +**refundableAmount** | **number** | | [optional] [default to undefined] +**canceledAt** | **string** | | [optional] [default to undefined] +**cancelStatus** | **string** | | [optional] [default to undefined] + +## Example + +```typescript +import { CancelData } from './api'; + +const instance: CancelData = { + transactionKey, + cancelReason, + cancelAmount, + refundableAmount, + canceledAt, + cancelStatus, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/PaymentData.md b/src/api/openapi/docs/PaymentData.md new file mode 100644 index 00000000..4db1d3be --- /dev/null +++ b/src/api/openapi/docs/PaymentData.md @@ -0,0 +1,36 @@ +# PaymentData + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**paymentKey** | **string** | | [optional] [default to undefined] +**orderId** | **string** | | [optional] [default to undefined] +**status** | **string** | | [optional] [default to undefined] +**method** | **string** | | [optional] [default to undefined] +**totalAmount** | **number** | | [optional] [default to undefined] +**balanceAmount** | **number** | | [optional] [default to undefined] +**approvedAt** | **string** | | [optional] [default to undefined] +**secret** | **string** | | [optional] [default to undefined] +**cancels** | [**Array<CancelData>**](CancelData.md) | | [optional] [default to undefined] + +## Example + +```typescript +import { PaymentData } from './api'; + +const instance: PaymentData = { + paymentKey, + orderId, + status, + method, + totalAmount, + balanceAmount, + approvedAt, + secret, + cancels, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/docs/TossWebhookApi.md b/src/api/openapi/docs/TossWebhookApi.md new file mode 100644 index 00000000..8e409c4a --- /dev/null +++ b/src/api/openapi/docs/TossWebhookApi.md @@ -0,0 +1,60 @@ +# TossWebhookApi + +All URIs are relative to *https://test-api.zeroone.it.kr* + +|Method | HTTP request | Description| +|------------- | ------------- | -------------| +|[**handleTossWebhook**](#handletosswebhook) | **POST** /api/v1/webhooks/toss | Toss Webhook 수신| + +# **handleTossWebhook** +> BaseResponseVoid handleTossWebhook(tossWebhookPayload) + +Toss Payments에서 발생한 이벤트를 수신합니다. secret 값으로 검증합니다. + +### Example + +```typescript +import { + TossWebhookApi, + Configuration, + TossWebhookPayload +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new TossWebhookApi(configuration); + +let tossWebhookPayload: TossWebhookPayload; // + +const { status, data } = await apiInstance.handleTossWebhook( + tossWebhookPayload +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **tossWebhookPayload** | **TossWebhookPayload**| | | + + +### Return type + +**BaseResponseVoid** + +### Authorization + +[bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: */* + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | OK | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/src/api/openapi/docs/TossWebhookPayload.md b/src/api/openapi/docs/TossWebhookPayload.md new file mode 100644 index 00000000..b653e662 --- /dev/null +++ b/src/api/openapi/docs/TossWebhookPayload.md @@ -0,0 +1,28 @@ +# TossWebhookPayload + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**eventType** | **string** | | [optional] [default to undefined] +**createdAt** | **string** | | [optional] [default to undefined] +**data** | [**PaymentData**](PaymentData.md) | | [optional] [default to undefined] +**paymentStatusChanged** | **boolean** | | [optional] [default to undefined] +**latestCancel** | [**CancelData**](CancelData.md) | | [optional] [default to undefined] + +## Example + +```typescript +import { TossWebhookPayload } from './api'; + +const instance: TossWebhookPayload = { + eventType, + createdAt, + data, + paymentStatusChanged, + latestCancel, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/src/api/openapi/models/cancel-data.ts b/src/api/openapi/models/cancel-data.ts new file mode 100644 index 00000000..a3d934f1 --- /dev/null +++ b/src/api/openapi/models/cancel-data.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface CancelData { + 'transactionKey'?: string; + 'cancelReason'?: string; + 'cancelAmount'?: number; + 'refundableAmount'?: number; + 'canceledAt'?: string; + 'cancelStatus'?: string; +} + diff --git a/src/api/openapi/models/index.ts b/src/api/openapi/models/index.ts index 4b9eba37..483d559b 100644 --- a/src/api/openapi/models/index.ts +++ b/src/api/openapi/models/index.ts @@ -24,6 +24,7 @@ export * from './base-response-study-space-is-participate-response'; export * from './base-response-temporal-token'; export * from './base-response-void'; export * from './calendar-day-status'; +export * from './cancel-data'; export * from './career-list-response'; export * from './career-response-dto'; export * from './change-hist-dto'; @@ -163,6 +164,7 @@ export * from './pageable'; export * from './participating-study-info'; export * from './participating-study-response'; export * from './participating-study-response-content'; +export * from './payment-data'; export * from './payment-history-response'; export * from './payment-search-condition'; export * from './phone-auth-response-dto'; @@ -223,6 +225,7 @@ export * from './thread-summary-response'; export * from './thread-summary-response-content'; export * from './today-study-data-response'; export * from './toss-payment-confirm-request'; +export * from './toss-webhook-payload'; export * from './update-group-study-member-greeting-request'; export * from './update-group-study-member-progress-request'; export * from './update-mission-response-schema'; diff --git a/src/api/openapi/models/payment-data.ts b/src/api/openapi/models/payment-data.ts new file mode 100644 index 00000000..881efd59 --- /dev/null +++ b/src/api/openapi/models/payment-data.ts @@ -0,0 +1,31 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { CancelData } from './cancel-data'; + +export interface PaymentData { + 'paymentKey'?: string; + 'orderId'?: string; + 'status'?: string; + 'method'?: string; + 'totalAmount'?: number; + 'balanceAmount'?: number; + 'approvedAt'?: string; + 'secret'?: string; + 'cancels'?: Array; +} + diff --git a/src/api/openapi/models/toss-webhook-payload.ts b/src/api/openapi/models/toss-webhook-payload.ts new file mode 100644 index 00000000..bd3385c4 --- /dev/null +++ b/src/api/openapi/models/toss-webhook-payload.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Zeroone API v1 + * v1 + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { CancelData } from './cancel-data'; +// May contain unused imports in some cases +// @ts-ignore +import type { PaymentData } from './payment-data'; + +export interface TossWebhookPayload { + 'eventType'?: string; + 'createdAt'?: string; + 'data'?: PaymentData; + 'paymentStatusChanged'?: boolean; + 'latestCancel'?: CancelData; +} + From ebeb45f4d85afe352a3ca0819b328684283ef30a Mon Sep 17 00:00:00 2001 From: yeun38 Date: Mon, 29 Dec 2025 22:13:07 +0900 Subject: [PATCH 030/211] =?UTF-8?q?=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(service)/payment/[id]/page.tsx | 31 +++++----------- src/components/payment/orderSummary.tsx | 15 +++----- .../payment/paymentActionClient.tsx | 36 ++++++++++--------- 3 files changed, 33 insertions(+), 49 deletions(-) diff --git a/src/app/(service)/payment/[id]/page.tsx b/src/app/(service)/payment/[id]/page.tsx index 1f46ef9d..2867a5c7 100644 --- a/src/app/(service)/payment/[id]/page.tsx +++ b/src/app/(service)/payment/[id]/page.tsx @@ -3,15 +3,6 @@ import { PaymentUserApi } from '@/api/openapi'; import OrderSummary from '@/components/payment/orderSummary'; import PaymentCheckoutPage from '@/components/payment/paymentActionClient'; import PriceSummary from '@/components/payment/priceSummary'; -import { getGroupStudyDetailInServer } from '@/features/study/group/api/get-group-study-detail.server'; - -interface Study { - id: string; - title: string; - desc: string; - price: number; - thumbnailUrl?: string; -} interface PaymentPageProps { params: Promise<{ id: string }>; @@ -22,16 +13,9 @@ export default async function CheckoutPage({ params }: PaymentPageProps) { const paymentUserApi = createApiServerInstance(PaymentUserApi); - try { - const { data: study } = await paymentUserApi.preparePayment(Number(id), { - amount: 2000, - }); - console.log('data', study); - } catch (error: any) { - console.log('에러 상태:', error.response?.status); - console.log('에러 응답:', error.response?.data); // 백엔드 에러 메시지 확인 - throw error; - } + const { data } = await paymentUserApi.preparePayment(Number(id), { + amount: 2000, + }); return (
    @@ -41,7 +25,10 @@ export default async function CheckoutPage({ params }: PaymentPageProps) {

    선택한 스터디

    - +
    {/* 서버 렌더: 결제 금액 */} @@ -49,12 +36,12 @@ export default async function CheckoutPage({ params }: PaymentPageProps) {

    결제 금액

    - +
    {/* 클라 렌더: 약관/결제수단/결제하기 */} -
    {}
    +
    {}
    diff --git a/src/components/payment/orderSummary.tsx b/src/components/payment/orderSummary.tsx index 4a9cf04f..d1126f38 100644 --- a/src/components/payment/orderSummary.tsx +++ b/src/components/payment/orderSummary.tsx @@ -1,24 +1,19 @@ interface Props { - study: { - title: string; - desc: string; - price: number; - thumbnailUrl?: string; - }; + groupStudyTitle: string; + amount: number; } -export default function OrderSummary({ study }: Props) { +export default function OrderSummary({ groupStudyTitle, amount }: Props) { return (
    -

    {study.title}

    -

    {study.desc}

    +

    {groupStudyTitle}

    - {study.price.toLocaleString()}원 + {amount.toLocaleString()}원

    diff --git a/src/components/payment/paymentActionClient.tsx b/src/components/payment/paymentActionClient.tsx index c6e86371..4f134140 100644 --- a/src/components/payment/paymentActionClient.tsx +++ b/src/components/payment/paymentActionClient.tsx @@ -2,21 +2,21 @@ import { loadTossPayments } from '@tosspayments/tosspayments-sdk'; import { useEffect, useState } from 'react'; +import { StudyPaymentPrepareResponse } from '@/api/openapi'; +import PaymentTermsModal from './PaymentTermsModal'; import Button from '../ui/button'; import Checkbox from '../ui/checkbox'; import { RadioGroup, RadioGroupItem } from '../ui/radio'; -import PaymentTermsModal from './PaymentTermsModal'; -interface Study { - id: string; - title: string; - desc: string; - price: number; - thumbnailUrl?: string; -} +// interface Study { +// paymentId: number; +// tossOrderId: string; +// groupStudyTitle: string; +// amount: number; +// } interface Props { - study: Study; + study: StudyPaymentPrepareResponse; } type PaymentMethod = 'CARD' | 'VBANK'; @@ -62,12 +62,15 @@ export default function PaymentCheckoutPage({ study }: Props) { method: paymentMethod, amount: { currency: 'KRW', - value: study.price, + value: study.amount, }, - orderId: study.id, - orderName: study.title, - successUrl: window.location.origin + '/payment/success', - failUrl: window.location.origin + '/payment/fail', + orderId: study.tossOrderId, + orderName: study.groupStudyTitle, + successUrl: + window.location.origin + + `/payment/success?paymentId=${study.paymentId}`, + failUrl: + window.location.origin + `/payment/fail?paymentId=${study.paymentId}`, customerEmail: 'customer123@gmail.com', customerName: '김토스', customerMobilePhone: '01012341234', @@ -89,7 +92,6 @@ export default function PaymentCheckoutPage({ study }: Props) { } }; - useEffect(() => { async function fetchPayment() { try { @@ -149,10 +151,10 @@ export default function PaymentCheckoutPage({ study }: Props) {