Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8164f03
fix: QnA 이미지 키 전송 오류 수정
Hyeonjun0527 May 16, 2026
3153604
Merge pull request #606 from code-zero-to-one/fix/e2e
HA-SEUNG-JEONG May 17, 2026
3ed8b2d
fix: QnA 제출 성공 시 blob URL 정리
Hyeonjun0527 May 17, 2026
4d311a6
fix: QnA 이미지 미리보기 해제
Hyeonjun0527 May 17, 2026
86a7cd4
fix: 커리큘럼 배지 테스트 locator 고정
Hyeonjun0527 May 17, 2026
1bf3cfe
fix: 커리큘럼 테스트 충돌 해결
Hyeonjun0527 May 17, 2026
08a5d12
Merge pull request #603 from code-zero-to-one/fix/lesson-qna-image-ke…
Hyeonjun0527 May 17, 2026
5bb61f6
fix: 노션 복붙 본문 유실 방지
Hyeonjun0527 May 17, 2026
d70f7ad
Merge pull request #609 from code-zero-to-one/fix/admin-lesson-editor…
Hyeonjun0527 May 17, 2026
b873a29
[builder-feed-update-and-delete] feat : chore(types): BuilderFeedUpda…
HA-SEUNG-JEONG May 17, 2026
b51a51a
[builder-feed-update-and-delete] feat : style(feed): biome import 정렬
HA-SEUNG-JEONG May 17, 2026
35c1941
[builder-feed-update-and-delete] fix : NaN feedId edit 모드 오분기 및 캐시 무효…
HA-SEUNG-JEONG May 17, 2026
8aef3ae
chore: 운영 릴리즈 기록 자동화
Hyeonjun0527 May 17, 2026
ecac9d1
fix: 릴리즈 리뷰 후속 보완 (#612)
Hyeonjun0527 May 17, 2026
f3a8189
fix: PR 613 main 병합 충돌 해소
Hyeonjun0527 May 17, 2026
8329b31
fix: 복합 HTML 붙여넣기 보존
Hyeonjun0527 May 17, 2026
0f72ddd
fix: 복합 붙여넣기 이미지 위치 보존
Hyeonjun0527 May 17, 2026
0946e33
fix: 복합 붙여넣기 표와 노션 이미지 안내 보존
Hyeonjun0527 May 17, 2026
1c29db5
chore: 운영 릴리즈 기록 수동 반영
Hyeonjun0527 May 17, 2026
f5ce0dc
fix: 운영 릴리즈 기록 메타데이터 보정
Hyeonjun0527 May 17, 2026
818ea49
[class] feat : [class] chore : .gitignore + .vscode/settings.json 업데이트
HA-SEUNG-JEONG May 18, 2026
3e1af00
[class] feat : Phase 1 — public 에셋 vibe-intro → class/ 경로 이동
HA-SEUNG-JEONG May 18, 2026
f8ae44c
[class] feat : [class] refactor : Phase 2 — components/pages/class/vi…
HA-SEUNG-JEONG May 18, 2026
fddbb80
[class] feat : Phase 3+4 — 클래스 라우트 vibe-intro → [slug] 동적 세그먼트 전환
HA-SEUNG-JEONG May 18, 2026
ca4ca7e
[class] fix : settings.json 복구
HA-SEUNG-JEONG May 18, 2026
238ecda
[class] feat : Merge branch 'develop' of https://github.com/code-zero…
HA-SEUNG-JEONG May 18, 2026
a01c861
[class] fix : import 오류 수정
HA-SEUNG-JEONG May 18, 2026
fbe76a2
[class] fix : feed 페이지 빌드 오류 수정 — use client 누락 및 vibe-intro 하드코딩 URL 제거
HA-SEUNG-JEONG May 18, 2026
ef24235
[class] fix : CodeRabbit 리뷰 반영 — QnA 취소 탭, 결제 성공 리다이렉트, 빌더 배지 토큰, 어드민…
HA-SEUNG-JEONG May 18, 2026
4406af0
[class] feat : [class] refactor : payment/success 페이지 isCoursePayment…
HA-SEUNG-JEONG May 18, 2026
0b0f5bb
[class] feat : [class] test : E2E 실패 7개 수정 및 QnA 작성 폼 커버리지 추가
HA-SEUNG-JEONG May 18, 2026
b708bcf
[class] feat : Merge branch 'main' of https://github.com/code-zero-to…
HA-SEUNG-JEONG May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Docker image build and push
- name: Docker image build and push
run: |
set -euo pipefail
Expand All @@ -108,12 +109,22 @@ jobs:
docker push "${{ steps.meta.outputs.frontend_image }}"
docker push "${FRONTEND_IMAGE_REPOSITORY}:prod"
docker push "${FRONTEND_IMAGE_REPOSITORY}:latest-prod"
set -euo pipefail
docker build -f Dockerfile.prod \
-t "${{ steps.meta.outputs.frontend_image }}" \
-t "${FRONTEND_IMAGE_REPOSITORY}:prod" \
-t "${FRONTEND_IMAGE_REPOSITORY}:latest-prod" \
.
docker push "${{ steps.meta.outputs.frontend_image }}"
docker push "${FRONTEND_IMAGE_REPOSITORY}:prod"
docker push "${FRONTEND_IMAGE_REPOSITORY}:latest-prod"

deploy-image-to-server:
deploy-image-to-server:
runs-on: ubuntu-latest
needs: build-and-push-image
env:
FRONTEND_IMAGE: zerooneitkr/zeroone-frontend:${{ needs.build-and-push-image.outputs.frontend_version }}-${{ needs.build-and-push-image.outputs.frontend_commit }}
FRONTEND_IMAGE: ${{ needs.build-and-push-image.outputs.frontend_image }}
RELEASE_ID: ${{ needs.build-and-push-image.outputs.release_id }}
DEPLOYED_AT: ${{ needs.build-and-push-image.outputs.deployed_at }}
FRONTEND_COMMIT: ${{ needs.build-and-push-image.outputs.frontend_commit }}
Expand Down Expand Up @@ -150,6 +161,17 @@ jobs:
node-version: '20'
cache: 'yarn'

- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: Install cloudflared
run: |
echo "Downloading cloudflared..."
Expand Down Expand Up @@ -247,16 +269,15 @@ jobs:
- name: Deploy frontend image to production server
run: |
set -euo pipefail
: "${FRONTEND_IMAGE:?FRONTEND_IMAGE is required}"
sshpass -p '${{ secrets.SSH_PASSWORD }}' ssh ssh.zeroone.it.kr \
"FRONTEND_IMAGE='$FRONTEND_IMAGE' bash -s" << 'EOF'
set -euo pipefail
sshpass -p '${{ secrets.SSH_PASSWORD }}' ssh ssh.zeroone.it.kr bash << 'EOF'
set -e
SUDO_PASSWORD='${{ secrets.SSH_PASSWORD }}'
: "${FRONTEND_IMAGE:?FRONTEND_IMAGE is required}"
FRONTEND_IMAGE='${{ needs.build-and-push-image.outputs.frontend_image }}'

echo "사용하지 않는 이미지, 네트워크 정리 중 (실행 중 컨테이너 보존, 볼륨 제외)..."
echo "$SUDO_PASSWORD" | sudo -S docker image prune -f || true
echo "$SUDO_PASSWORD" | sudo -S docker network prune -f || true
echo "사용하지 않는 컨테이너, 이미지, 네트워크 정리 중 (볼륨제외)..."
echo "$SUDO_PASSWORD" | sudo -S docker stop frontend-prod || true
echo "$SUDO_PASSWORD" | sudo -S docker rm frontend-prod || true
echo "$SUDO_PASSWORD" | sudo -S docker system prune -a -f

cd ~/front/study-platform-client-prod || {
echo "디렉토리가 없습니다. 생성합니다..."
Expand Down Expand Up @@ -289,14 +310,14 @@ jobs:
restart: unless-stopped
COMPOSE

echo "도커 이미지 pull: $FRONTEND_IMAGE"
echo "$SUDO_PASSWORD" | sudo -S docker pull "$FRONTEND_IMAGE"

echo "기존 컨테이너 교체"
echo "$SUDO_PASSWORD" | sudo -S docker compose -f docker-compose.prod.yml down || true
echo "$SUDO_PASSWORD" | sudo -S docker stop frontend-prod || true
echo "$SUDO_PASSWORD" | sudo -S docker rm frontend-prod || true

echo "도커 이미지 pull: $FRONTEND_IMAGE"
echo "$SUDO_PASSWORD" | sudo -S docker pull "$FRONTEND_IMAGE"

echo "도커 컴포즈 재시작"
echo "$SUDO_PASSWORD" | sudo -S docker compose -f docker-compose.prod.yml up -d
echo "$SUDO_PASSWORD" | sudo -S docker system prune -f || true
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,9 @@ e2e/fixtures/auth.json
playwright-report/
test-results/

.claude/
.claude/commands
.claude/agents
.claude/skills
.playwright-mcp
.omc
/docs
6 changes: 3 additions & 3 deletions e2e/class/journey-map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ test.describe('안내창 상태 렌더링 @auth', () => {
await expect(page.getByText(/NEXT → Lesson 02/)).toBeVisible();
await expect(page.getByText('Chapter3까지 무료 코스!')).not.toBeVisible();
await expect(
page.getByRole('link', { name: '결제하기' }),
page.getByRole('button', { name: '결제하기' }),
).not.toBeVisible();
});

Expand Down Expand Up @@ -455,7 +455,7 @@ test.describe('안내창 상태 렌더링 @auth', () => {
await expect(page.getByText(/NEXT → Lesson 01/)).toBeVisible();
await expect(page.getByText('Chapter3까지 무료 코스!')).not.toBeVisible();
await expect(
page.getByRole('link', { name: '결제하기' }),
page.getByRole('button', { name: '결제하기' }),
).not.toBeVisible();
});

Expand All @@ -473,7 +473,7 @@ test.describe('안내창 상태 렌더링 @auth', () => {
await gotoAndWaitForData(page);

await expect(
page.getByRole('link', { name: '결제하기' }),
page.getByRole('button', { name: '결제하기' }),
).not.toBeVisible();
});

Expand Down
1 change: 1 addition & 0 deletions e2e/class/payment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@
page: Page,
opts: {
detail?: ReturnType<typeof makeCourseDetail>;
prepare?: ReturnType<typeof makePaymentPrepare> | null;

Check warning on line 134 in e2e/class/payment.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Usage of "null" is deprecated except when describing legacy APIs; use "undefined" instead
confirm?: ReturnType<typeof makePaymentConfirm> | null;

Check warning on line 135 in e2e/class/payment.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Usage of "null" is deprecated except when describing legacy APIs; use "undefined" instead
} = {},
) {
const detail = opts.detail ?? makeCourseDetail();
Expand Down Expand Up @@ -267,6 +267,7 @@
page,
}) => {
await page.locator('input[name="tosAgreed"]').check({ force: true });
await page.locator('input[type="radio"][value="CARD"]').check();
await page.getByPlaceholder('이름을 입력해주세요').fill('홍길동');
await page.locator('#buyerEmail').fill('test@example.com');
await page.getByPlaceholder('01012345678').fill('01012345678');
Expand Down
94 changes: 94 additions & 0 deletions e2e/class/qna.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const COURSE_ID = 1;
const QNA_ID = 77;
const QNA_LIST_PATH = '/class/vibe-intro/home?tab=qna';
const QNA_DETAIL_PATH = `/class/vibe-intro/qa/${QNA_ID}`;
const QNA_WRITE_PATH = '/class/vibe-intro/qna/write';

// ─── Localhost auth cookie injection ─────────────────────────────────────────

Expand Down Expand Up @@ -235,3 +236,96 @@ test.describe('QnA 상세', () => {
expect(page.url()).toContain('tab=qna');
});
});

// ─── Chunk 3: QnA 작성 폼 유효성 검사 ────────────────────────────────────────

test.describe('QnA 작성 폼 유효성 검사 @auth', () => {
test.beforeEach(async ({ page }) => {
await page.route(/\/courses\//, async (route) => {
const url = route.request().url();
if (url.includes('/curriculum')) {
await route.fulfill({
json: {
content: {
courseId: COURSE_ID,
durationDays: 30,
totalChapters: 1,
totalLessons: 1,
chapters: [
{
chapterId: 10,
order: 1,
chapterNumber: 1,
title: '시작하기',
description: null,
estimatedMinutes: 18,
lessons: [
{
lessonId: 101,
order: 1,
title: '기초 세팅',
description: null,
isFree: true,
isLocked: false,
estimatedMinutes: 18,
},
],
},
],
},
},
});
} else if (/\/courses\/vibe-intro$/.test(url)) {
await route.fulfill({ json: makeCourseDetail() });
} else {
await route.continue();
}
});
await page.goto(QNA_WRITE_PATH, { waitUntil: 'load' });
});

test('레슨 미선택 → 제출 버튼 비활성화', async ({ page }) => {
const submitBtn = page
.getByRole('button', { name: /등록|제출|질문/ })
.first();
await expect(submitBtn).toBeDisabled({ timeout: 5000 });
});

test('내용 입력 후 제출 → POST /courses/{courseId}/qnas 호출', async ({
page,
}) => {
await page.route(/\/qnas/, async (route) => {
if (route.request().method() === 'POST') {
await route.fulfill({ json: { content: { qnaId: QNA_ID } } });
} else {
await route.continue();
}
});

// Open lesson selector and pick first option
const selector = page.locator('select, [role="combobox"]').first();
await selector.click();
const firstOption = page.getByRole('option').first();
if (await firstOption.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstOption.click();
}

await page
.locator('textarea, [contenteditable="true"]')
.first()
.fill('레슨을 수강하며 궁금한 점이 생겼습니다.');

const [response] = await Promise.all([
page.waitForResponse(
(r) => r.url().includes('/qnas') && r.request().method() === 'POST',
{ timeout: 5000 },
),
page
.getByRole('button', { name: /등록|제출|질문/ })
.first()
.click(),
]);

expect(response.status()).toBe(200);
});
});
2 changes: 1 addition & 1 deletion e2e/group-study/create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ test.describe('멘토스터디 개설 @auth', () => {

test('PREMIUM_STUDY 가격 필드 포함 전체 제출', async ({ page }) => {
await openPremiumStudyModal(page);
await fillStep1(page);
await fillStep1(page, 'MENTOR_STUDY');

const priceInput = page.getByPlaceholder('10,000');
await priceInput.fill('50000');
Expand Down
File renamed without changes
Binary file added public/class/detail/audit/node-142-3114.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/audit/node-210-2643.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/audit/node-225-4596.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/audit/node-306-2830.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/audit/node-33-1925.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/audit/node-771-23642.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/audit/node-865-33747.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/audit/node-865-34214.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/class/detail/class-detail-figma-ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes.
Binary file added public/class/vibe-intro/feed-more-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ interface Props {
open: boolean;
onClose: () => void;
courseTitle: string;
courseSlug: string;
chapters: CourseDrawerChapterResponse[];
expandedChapters: Set<number>;
onToggleChapter: (chapterId: number) => void;
isLoading?: boolean;
}

const ASSET = '/class/vibe-intro/curriculum';
const ASSET = '/class/curriculum';

function LessonBadge({ lesson }: { lesson: CourseDrawerLessonResponse }) {
const accessible = !lesson.isLocked;
Expand Down Expand Up @@ -126,8 +127,10 @@ function ChapterHeader({

function ChapterLessons({
lessons,
courseSlug,
}: {
lessons: CourseDrawerLessonResponse[];
courseSlug: string;
}) {
return (
<div className="relative bg-background-default pb-250 pt-375">
Expand All @@ -140,7 +143,7 @@ function ChapterLessons({
return (
<li key={lesson.lessonId}>
<Link
href={`/class/vibe-intro/lesson/${lesson.lessonId}`}
href={`/class/${courseSlug}/lesson/${lesson.lessonId}`}
className="relative flex min-w-0 items-center gap-200 py-175 pl-550"
>
<Image
Expand Down Expand Up @@ -176,6 +179,7 @@ export function CurriculumDrawer({
open,
onClose,
courseTitle,
courseSlug,
chapters,
expandedChapters,
onToggleChapter,
Expand Down Expand Up @@ -238,7 +242,12 @@ export function CurriculumDrawer({
expanded={expanded}
onToggle={() => onToggleChapter(chapter.chapterId)}
/>
{expanded && <ChapterLessons lessons={chapter.lessons} />}
{expanded && (
<ChapterLessons
lessons={chapter.lessons}
courseSlug={courseSlug}
/>
)}
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import Image from 'next/image';
import { useState } from 'react';
import {
AuthorAvatar,
ROLE_LABELS,
RoleBadge,
} from '@/app/(landing)/class/vibe-intro/_components/builder-feed-utils';
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
import {
FeedCommentIcon,
FeedHeartIcon,
FeedShareIcon,
} from '@/components/common/ui/icons/course-icons';
import {
AuthorAvatar,
ROLE_LABELS,
RoleBadge,
} from '@/components/pages/class/utils/builder-feed-utils';
import type { BuilderFeedPreviewItemResponse } from '@/types/api/course.types';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import { Heart, MessageCircle, X } from 'lucide-react';
import Image from 'next/image';
import MarkdownContentCore from '@/components/common/ui/rich-text/markdown-content-core';
import {
AuthorAvatar,
formatRelativeTime,
ROLE_LABELS,
RoleBadge,
} from '@/app/(landing)/class/vibe-intro/_components/builder-feed-utils';
import MarkdownContentCore from '@/components/common/ui/rich-text/markdown-content-core';
} from '@/components/pages/class/utils/builder-feed-utils';
import { useGetBuilderFeedDetail } from '@/hooks/queries/course/course-api';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function RatingBox({ rating, onChange }: Props) {
<Image
src={
star <= display
? '/class/vibe-intro/star-enabled.svg'
: '/class/vibe-intro/star-disabled.svg'
? '/class/star-enabled.svg'
: '/class/star-disabled.svg'
}
alt=""
aria-hidden="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export function LessonReviewForm({
>
{submitDisabled && (
<Image
src="/class/vibe-intro/lesson-lock.svg"
src="/class/lesson-lock.svg"
alt=""
aria-hidden="true"
width={24}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function LessonTopBar({
className="group flex h-800 w-800 shrink-0 flex-col items-center justify-center gap-25 bg-background-brand-default text-text-inverse"
>
<Image
src="/class/vibe-intro/checklist.svg"
src="/class/checklist.svg"
alt=""
aria-hidden="true"
width={32}
Expand Down
Loading
Loading