+
{careers.map((career) => career.careerTitle).join(' / ')}
-
+
+
+
}
+ iconPosition="left"
+ size="M"
+ className="rounded-lg gap-1 px-3 py-2 w-[156px] h-[39px]"
+ onClick={handleCardClick}
+ />
+
);
};
diff --git a/src/app/expert/detail/components/ExpertDetailSidebar.tsx b/src/app/expert/detail/components/ExpertDetailSidebar.tsx
index 09c1223..eb5bb3e 100644
--- a/src/app/expert/detail/components/ExpertDetailSidebar.tsx
+++ b/src/app/expert/detail/components/ExpertDetailSidebar.tsx
@@ -50,7 +50,7 @@ const ExpertDetailSidebar = ({ expert }: ExpertDetailSidebarProps) => {
const ButtonIcon = shouldShowCreateButton
? WhitePlus
: hasRequested
- ? GrayPlus
+ ? GrayCheck
: disabled && !isSelectedPlanOver70
? GrayCheck
: disabled
diff --git a/src/app/globals.css b/src/app/globals.css
index 7741acb..44cb7aa 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -7,7 +7,7 @@
@theme inline {
/* Colors */
- --color-primary-50: #f8f6ff;
+ --color-primary-50: #eae5ff;
--color-primary-100: #d2caff;
--color-primary-200: #bdb1ff;
--color-primary-300: #9f8dff;
diff --git a/src/app/mypage/components/MyAccount.tsx b/src/app/mypage/components/MyAccount.tsx
index 65dc11e..47bdd1d 100644
--- a/src/app/mypage/components/MyAccount.tsx
+++ b/src/app/mypage/components/MyAccount.tsx
@@ -1,18 +1,15 @@
'use client';
import Image from 'next/image';
-import React, { useState } from 'react';
-import PayHistoryModal from './PayHistoryModal';
import { useUserStore } from '@/store/user.store';
import Naver from '@/assets/icons/naver_profile.svg';
import KaKao from '@/assets/icons/kakao_profile.svg';
const MyAccount = () => {
- const [isModal, setIsModal] = useState(false);
const { user } = useUserStore();
return (
-
-
+
+
내 계정
@@ -49,29 +46,6 @@ const MyAccount = () => {
-
-
-
-
요금제 관리
-
-
-
-
-
-
-
Lite 요금제
-
- 잔여횟수 총 1/5회
-
-
-
-
- {isModal &&
setIsModal(false)} />}
);
};
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 772740c..74074e8 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -3,6 +3,7 @@ import LandingBlackSection from './_components/landing/LandingBlackSection';
import LandingChecklist from './_components/landing/LandingChecklist';
import LandingPaySection from './_components/landing/LandingPaySection';
import LandingRelation from './_components/landing/LandingRelation';
+//워크플로우 테스트
const page = () => {
return (
diff --git a/src/app/pay/complete/page.tsx b/src/app/pay/complete/page.tsx
index 6febf70..8430769 100644
--- a/src/app/pay/complete/page.tsx
+++ b/src/app/pay/complete/page.tsx
@@ -66,7 +66,23 @@ function PayCompleteInner() {
return;
}
- setState('FAIL');
+ // setState('FAIL');
+
+ setState('LOADING');
+ // 프로모션 기간 임시 결제완료 처리
+ const dummyData: OrderConfirmResponseDto = {
+ buyerId: 0,
+ paymentKey: 'dummy-payment-key',
+ orderId: 'dummy-order-id',
+ amount: 0,
+ status: 'PAID',
+ approvedAt: Date.now(),
+ receiptUrl: null,
+ method: 'CARD',
+ provider: null,
+ };
+ setData(dummyData);
+ setState('SUCCESS');
}, [searchParams]);
useEffect(() => {
diff --git a/src/app/pay/page.tsx b/src/app/pay/page.tsx
index d84a7d9..ca74d88 100644
--- a/src/app/pay/page.tsx
+++ b/src/app/pay/page.tsx
@@ -1,6 +1,7 @@
'use client';
import Script from 'next/script';
+import { useRouter } from 'next/navigation';
import Button from '../_components/common/Button';
import OrderDetails from './components/OrderDetails';
import PaymentAmount from './components/PaymentAmount';
@@ -17,6 +18,7 @@ const EXPERT_PRODUCT_CODE: UsageProductCode = 'AI_REPORT_1';
const Page = () => {
const { selectedMentor } = useExpertStore();
+ const router = useRouter();
const planId = useBusinessStore((s) => s.planId);
const expertId = selectedMentor?.id ?? null;
@@ -35,14 +37,21 @@ const Page = () => {
return;
}
- try {
- await startPayment({
- productCode: EXPERT_PRODUCT_CODE,
- method: DEFAULT_METHOD,
- });
- } catch (e) {
- console.error('결제 시작 에러:', e);
- alert('결제를 시작하는 데 실패했습니다. 다시 시도해 주세요.');
+ // 프로모션 기간동안
+ // try {
+ // await startPayment({
+ // productCode: EXPERT_PRODUCT_CODE,
+ // method: DEFAULT_METHOD,
+ // });
+ // } catch (e) {
+ // console.error('결제 시작 에러:', e);
+ // alert('결제를 시작하는 데 실패했습니다. 다시 시도해 주세요.');
+ // }
+
+ if (planId != null && expertId != null) {
+ router.push(`/pay/complete?planId=${planId}&expertId=${expertId}`);
+ } else {
+ router.push('/pay/complete');
}
};
diff --git a/src/assets/icons/black_check.svg b/src/assets/icons/black_check.svg
new file mode 100644
index 0000000..c006b30
--- /dev/null
+++ b/src/assets/icons/black_check.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/polygon.svg b/src/assets/icons/polygon.svg
new file mode 100644
index 0000000..0e62b09
--- /dev/null
+++ b/src/assets/icons/polygon.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/hooks/useCountdown.ts b/src/hooks/useCountdown.ts
new file mode 100644
index 0000000..ea9b1bc
--- /dev/null
+++ b/src/hooks/useCountdown.ts
@@ -0,0 +1,39 @@
+import { useState, useEffect } from 'react';
+
+const calculateTimeLeft = (targetDate: string) => {
+ if (typeof window === 'undefined') {
+ return { days: 0, hours: 0, minutes: 0, seconds: 0 };
+ }
+
+ const target = new Date(targetDate).getTime();
+ const now = Date.now();
+ const diff = target - now;
+
+ if (diff > 0) {
+ return {
+ days: Math.floor(diff / (1000 * 60 * 60 * 24)),
+ hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
+ minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
+ seconds: Math.floor((diff % (1000 * 60)) / 1000),
+ };
+ }
+
+ return { days: 0, hours: 0, minutes: 0, seconds: 0 };
+};
+
+export const useCountdown = (targetDate: string) => {
+ const [timeLeft, setTimeLeft] = useState(() => calculateTimeLeft(targetDate));
+
+ useEffect(() => {
+ const updateTimer = () => {
+ setTimeLeft(calculateTimeLeft(targetDate));
+ };
+
+ updateTimer();
+ const interval = setInterval(updateTimer, 1000);
+
+ return () => clearInterval(interval);
+ }, [targetDate]);
+
+ return timeLeft;
+};
diff --git a/src/lib/business/converter/editorToHtml.ts b/src/lib/business/converter/editorToHtml.ts
index 1746326..2bcfff5 100644
--- a/src/lib/business/converter/editorToHtml.ts
+++ b/src/lib/business/converter/editorToHtml.ts
@@ -77,11 +77,30 @@ export const convertToHtml = (node: JSONNode | null | undefined): string => {
return `
${content}`;
}
+ // editorToHtml.ts 수정
+
if (node.type === 'bulletList') {
const items = (node.content || [])
.map((item) => {
- const itemContent = (item.content || []).map((child) => convertToHtml(child)).join('');
- return itemContent ? `
${itemContent}` : '';
+ const itemContent = (item.content || [])
+ .map((child) => {
+ // 빈 paragraph는 빈 문자열로 변환 (빈 줄 방지)
+ if (child.type === 'paragraph') {
+ const paragraphContent = (child.content || [])
+ .map((c) => convertToHtml(c))
+ .join('');
+ // 빈 paragraph는 빈 문자열 반환 (불릿 점은 유지되지만 빈 줄은 생성 안 됨)
+ if (!paragraphContent || paragraphContent.trim() === '') {
+ return '';
+ }
+ return `
${paragraphContent}
`;
+ }
+ return convertToHtml(child);
+ })
+ .join('');
+
+ // 리스트 아이템은 내용이 없어도 유지 (빈 불릿 점 표시)
+ return `
${itemContent}`;
})
.filter(Boolean);
const listStyle = 'list-style-type: disc; padding-left: 1.5rem; margin: 0 0 0.75rem 0;';
@@ -91,8 +110,22 @@ export const convertToHtml = (node: JSONNode | null | undefined): string => {
if (node.type === 'orderedList') {
const items = (node.content || [])
.map((item) => {
- const itemContent = (item.content || []).map((child) => convertToHtml(child)).join('');
- return itemContent ? `
${itemContent}` : '';
+ const itemContent = (item.content || [])
+ .map((child) => {
+ if (child.type === 'paragraph') {
+ const paragraphContent = (child.content || [])
+ .map((c) => convertToHtml(c))
+ .join('');
+ if (!paragraphContent || paragraphContent.trim() === '') {
+ return '';
+ }
+ return `
${paragraphContent}
`;
+ }
+ return convertToHtml(child);
+ })
+ .join('');
+
+ return `
${itemContent}`;
})
.filter(Boolean);
const listStyle = 'list-style-type: decimal; padding-left: 1.5rem; margin: 0 0 0.75rem 0;';
@@ -205,13 +238,13 @@ export const convertToHtml = (node: JSONNode | null | undefined): string => {
// width와 height가 있으면 그대로 사용, 없으면 기본 스타일 사용
let style = '';
if (width && height) {
- style = `width: ${width}px; height: ${height}px;`;
+ style = `width: ${width}px; height: ${height}px; object-fit: contain;`;
} else if (width) {
- style = `width: ${width}px; height: auto;`;
+ style = `width: ${width}px; height: auto; object-fit: contain;`;
} else if (height) {
- style = `width: auto; height: ${height}px;`;
+ style = `width: auto; height: ${height}px; object-fit: contain;`;
} else {
- style = 'max-width: 400px; height: auto;';
+ style = 'max-width: 400px; height: auto; object-fit: contain;';
}
// 캡션이 있으면 포함
diff --git a/src/lib/business/editor/editorConstants.ts b/src/lib/business/editor/editorConstants.ts
index 55dddab..6885ba9 100644
--- a/src/lib/business/editor/editorConstants.ts
+++ b/src/lib/business/editor/editorConstants.ts
@@ -15,6 +15,7 @@ import {
ResizableImage,
SelectTableOnBorderClick,
EnsureTrailingParagraph,
+ CleanEmptyParagraphsInListItems,
} from './extensions';
import { uploadImage } from '@/lib/imageUpload';
import { getImageDimensions, clampImageDimensions } from '@/lib/getImageDimensions';
@@ -36,6 +37,7 @@ export const COMMON_EXTENSIONS = [
TableCell,
SelectTableOnBorderClick,
EnsureTrailingParagraph,
+ CleanEmptyParagraphsInListItems,
];
// 간단한 에디터 확장 (하이라이트, 볼드, 색상만 가능, 헤딩/표/이미지 비활성화)
diff --git a/src/lib/business/editor/extensions.ts b/src/lib/business/editor/extensions.ts
index 7507668..7bc002a 100644
--- a/src/lib/business/editor/extensions.ts
+++ b/src/lib/business/editor/extensions.ts
@@ -998,7 +998,7 @@ export const SelectTableOnBorderClick = Extension.create({
},
});
-// 문서 끝에 항상 빈 paragraph 유지
+// 표와 이미지 다음에만 빈 paragraph 유지
export const EnsureTrailingParagraph = Extension.create({
name: 'ensure-trailing-paragraph',
addProseMirrorPlugins() {
@@ -1022,8 +1022,54 @@ export const EnsureTrailingParagraph = Extension.create({
return null;
}
- const tr = newState.tr.insert(doc.content.size, paragraphType.create());
- return tr;
+ // 표나 이미지 다음에만 paragraph 추가
+ if (lastChild.type.name === 'table' || lastChild.type.name === 'image') {
+ const tr = newState.tr.insert(doc.content.size, paragraphType.create());
+ return tr;
+ }
+
+ return null;
+ },
+ }),
+ ];
+ },
+});
+
+// 리스트 아이템 내부의 빈 paragraph 제거
+export const CleanEmptyParagraphsInListItems = Extension.create({
+ name: 'clean-empty-paragraphs-in-list-items',
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ appendTransaction: (transactions, oldState, newState) => {
+ if (!transactions.some((tr) => tr.docChanged)) {
+ return null;
+ }
+
+ const tr = newState.tr;
+ let modified = false;
+
+ newState.doc.descendants((node, pos) => {
+ if (node.type.name === 'listItem') {
+ const emptyParagraphs: number[] = [];
+
+ node.forEach((child, offset) => {
+ if (child.type.name === 'paragraph' && child.content.size === 0) {
+ emptyParagraphs.push(pos + offset + 1);
+ }
+ });
+
+ // 빈 paragraph가 여러 개 있으면 하나만 남기고 나머지 제거
+ if (emptyParagraphs.length > 1) {
+ for (let i = emptyParagraphs.length - 1; i > 0; i--) {
+ tr.delete(emptyParagraphs[i], emptyParagraphs[i] + 1);
+ modified = true;
+ }
+ }
+ }
+ });
+
+ return modified ? tr : null;
},
}),
];
diff --git a/src/types/expert/expert.props.ts b/src/types/expert/expert.props.ts
index 36d93ab..61f733a 100644
--- a/src/types/expert/expert.props.ts
+++ b/src/types/expert/expert.props.ts
@@ -11,6 +11,7 @@ export interface MentorProps {
tags: string[];
categories: string[];
workingperiod: number;
+ oneLineIntroduction: string;
}
export interface MentorCardProps {
@@ -21,6 +22,7 @@ export interface MentorCardProps {
orderIndex: number;
careerTitle: string;
}[];
+ oneLineIntroduction: string;
tags: string[];
workingperiod: number;
status: 'active' | 'done';
@@ -36,4 +38,5 @@ export const adaptMentor = (e: getExpertResponse) => ({
.map(mappingKorea)
.filter(Boolean) as TabLabel[],
workingperiod: e.workedPeriod,
+ oneLineIntroduction: e.oneLineIntroduction,
});