Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,6 @@ module.exports = {
'no-unused-expressions': ['error', { allowTernary: true }],

// console.log 등 디버깅 코드 커밋 방지 (warn/error는 허용)
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
},
};
1 change: 0 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@

yarn lint-staged
23 changes: 20 additions & 3 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'path';
import { withSentryConfig } from '@sentry/nextjs';
import type { NextConfig } from 'next';
import type { RemotePattern } from 'next/dist/shared/lib/image-config';
Expand Down Expand Up @@ -83,13 +84,19 @@ const nextConfig: NextConfig = {
source: '/(.*)',
headers: [
// Report-Only 모드: 위반을 차단하지 않고 콘솔에 경고만 출력. 검증 완료 후 CSP로 전환 예정.
{ key: 'Content-Security-Policy-Report-Only', value: cspDirectives },
{
key: 'Content-Security-Policy-Report-Only',
value: cspDirectives,
},
// [클릭재킹 방지] 동일 출처의 iframe만 허용
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
// [MIME 스니핑 방지] 브라우저가 Content-Type을 임의로 변경하는 행위 차단
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// [정보 유출 방지] 크로스 오리진 요청 시 origin만 전송
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
// [HTTPS 강제] 1년간 HTTPS만 허용 (프로덕션에서 유효)
{
key: 'Strict-Transport-Security',
Expand Down Expand Up @@ -190,7 +197,9 @@ const nextConfig: NextConfig = {
{
test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
resourceQuery: {
not: [...fileLoaderRule.resourceQuery.not, /url/],
},
use: [
{
loader: '@svgr/webpack',
Expand All @@ -204,6 +213,14 @@ const nextConfig: NextConfig = {
);
fileLoaderRule.exclude = /\.svg$/i;

config.resolve = {
...config.resolve,
alias: {
...config.resolve?.alias,
public: path.join(__dirname, 'public'),
},
};

return config;
},
};
Expand Down
3 changes: 1 addition & 2 deletions src/components/card/mission-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { ComponentProps, SyntheticEvent } from 'react';

import type { MissionListResponse } from '@/api/openapi/models';
import Badge from '@/components/common/ui/badge';
import Button from '@/components/common/ui/button';
import Tooltip from '@/components/common/ui/tooltip';
import { cn } from '../common/ui/(shadcn)/lib/utils';

Expand Down Expand Up @@ -293,7 +292,7 @@ function MissionCardContent({
</span>
<Badge color={statusConfig.color}>{statusConfig.label}</Badge>
</div>
<span className="text-text-subtlest font-designer-12r">
<span className="text-text-subtlest text-left font-designer-12r">
제출 기간 : {formatDate(startDate)} ~ {formatDate(endDate)}
</span>
</div>
Expand Down
3 changes: 1 addition & 2 deletions src/components/card/study-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,13 @@ export default function StudyCard({ study, href, onClick }: StudyCardProps) {
status,
maxMembersCount = 0,
approvedCount = 0,
remainingSlots,
startDate,
} = {},
} = study ?? {};
const studyType = type as StudyType;

const isCompleted = study.basicInfo?.status === 'COMPLETED';
const remaining = remainingSlots ?? maxMembersCount - approvedCount;
const remaining = Math.max(0, maxMembersCount - approvedCount);

return (
<Link
Expand Down
13 changes: 7 additions & 6 deletions src/components/common/ui/editor/markdown-content.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import 'highlight.js/styles/github.css';
import DOMPurify from 'dompurify';
import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';
import hljs from 'highlight.js/lib/core';
import bash from 'highlight.js/lib/languages/bash';
import c from 'highlight.js/lib/languages/c';
Expand Down Expand Up @@ -60,7 +60,7 @@ interface MarkdownContentProps {
emptyMessage?: string;
}

const SANITIZE_OPTIONS: DOMPurify.Config = {
const SANITIZE_OPTIONS: DOMPurifyConfig = {
ALLOWED_TAGS: [
'a',
'blockquote',
Expand Down Expand Up @@ -198,7 +198,7 @@ function MarkdownContent({
return typeof rendered === 'string' ? rendered : '';
})();

const sanitized = DOMPurify.sanitize(html, SANITIZE_OPTIONS);
const sanitized = String(DOMPurify.sanitize(html, SANITIZE_OPTIONS));

return applyPostSanitizeAttributes({
originalHtml: html,
Expand All @@ -207,11 +207,13 @@ function MarkdownContent({
}, [hasContent, normalizedContent]);

useEffect(() => {
if (!containerRef.current) {
const container = containerRef.current;
if (!container) {
return;
}

containerRef.current.querySelectorAll('pre code').forEach((block) => {
container.innerHTML = sanitizedHtml;
container.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block as HTMLElement);
});
}, [sanitizedHtml]);
Expand Down Expand Up @@ -247,7 +249,6 @@ function MarkdownContent({
'[&_hr]:border-border-subtle [&_hr]:my-200',
className,
)}
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
);
}
Expand Down
13 changes: 8 additions & 5 deletions src/components/common/ui/study-active-ticker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ interface StudyActiveTickerProps {
startDate: string;
viewCount?: number;
className?: string;
remainingSlot?: number;
}

export default function StudyActiveTicker({
approvedCount,
maxMembersCount,
viewCount = 0,
remainingSlot = 0,
className = '',
}: StudyActiveTickerProps) {
const remaining = Math.max(0, maxMembersCount - approvedCount);
const totalCount = maxMembersCount - remaining;
const safeApprovedCount = Number.isFinite(approvedCount) ? approvedCount : 0;
const safeMaxMembersCount = Number.isFinite(maxMembersCount)
? maxMembersCount
: 0;
const remaining = Math.max(0, safeMaxMembersCount - safeApprovedCount);
const totalCount = safeApprovedCount;

const [currentIndex, setCurrentIndex] = useState(0);

const messages = [
Expand All @@ -45,7 +48,7 @@ export default function StudyActiveTicker({
</span>
이 가입했고 현재{' '}
<span className="font-designer-16b text-text-error">
{remainingSlot}자리
{remaining}자리
</span>{' '}
남았어요.
</p>
Expand Down
1 change: 0 additions & 1 deletion src/components/one-on-one/one-on-one-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
} from 'lucide-react';
import dynamic from 'next/dynamic';
import Image from 'next/image';
import Link from 'next/link';
import React, { useState, useEffect } from 'react';
import { cn } from '@/components/common/ui/(shadcn)/lib/utils';
import UserAvatar from '@/components/common/ui/avatar';
Expand Down
1 change: 0 additions & 1 deletion src/components/pages/group-study-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,6 @@ export default function StudyDetailPage({
<div className={`mt-500 ${DETAIL_CONTENT_WIDTH}`}>
<StudyActiveTicker
approvedCount={studyDetail.basicInfo.approvedCount}
remainingSlot={studyDetail.basicInfo.remainingSlots}
maxMembersCount={studyDetail.basicInfo.maxMembersCount}
startDate={studyDetail.basicInfo.startDate}
viewCount={studyDetail.viewCount}
Expand Down
3 changes: 2 additions & 1 deletion src/components/pages/premium-study-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,12 @@ export default function PremiumStudyDetailPage({
group_study_id: String(groupStudyId),
});
showToast('스터디가 종료되었습니다.');
setConfirmAction(null);
router.push('/premium-study');
},
onError: () => {
showToast('스터디 종료에 실패하였습니다.', 'error');
},
onSettled: () => {
setConfirmAction(null);
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import 'highlight.js/styles/github.css';
import DOMPurify from 'dompurify';
import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';
import hljs from 'highlight.js/lib/core';
import bash from 'highlight.js/lib/languages/bash';
import c from 'highlight.js/lib/languages/c';
Expand Down Expand Up @@ -44,7 +44,7 @@ hljs.registerLanguage('rust', rust);
hljs.registerLanguage('swift', swift);
hljs.registerLanguage('dart', dart);

const SANITIZE_OPTIONS: DOMPurify.Config = {
const SANITIZE_OPTIONS: DOMPurifyConfig = {
ALLOWED_TAGS: [
'a',
'blockquote',
Expand Down Expand Up @@ -186,7 +186,7 @@ export const normalizeMentorMarkdownToSanitizedHtml = (content: unknown) => {
html = typeof rendered === 'string' ? rendered : '';
}

const sanitized = DOMPurify.sanitize(html, SANITIZE_OPTIONS);
const sanitized = String(DOMPurify.sanitize(html, SANITIZE_OPTIONS));

return applyPostSanitizeAttributes({
originalHtml: html,
Expand Down
4 changes: 4 additions & 0 deletions src/types/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.css' {
const content: Record<string, string>;
export default content;
}
8 changes: 4 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
{
"exclude": ["node_modules/*", "build", "jest", "dist"],
"compilerOptions": {
"target": "es5", // ts파일 변환 시 어떤 버전의 JS로
"target": "ES2017", // ts파일 변환 시 어떤 버전의 JS로
"module": "esnext", // import 문법을 어떻게 할 것이냐
"moduleResolution": "node",
"baseUrl": ".",
"moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"public/*": ["./public/*"]
},
// lib옵션은 컴파일에 필요한 javascrtipt 내장 라이브러리를 지정할 수 있다.
// 이 프로퍼티가 지정되어 있지 않다면 'target' 프로퍼티에 지정된 버전에 따라 필요한 타입 정의들에 대한 정보가 자동 지정된다.
Expand Down
Loading
Loading