diff --git a/.husky/pre-commit b/.husky/pre-commit index cb0b5765..68038af8 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -24,8 +24,7 @@ if [ "$RUN_WEB" -eq 1 ]; then fi if [ "$RUN_ADMIN" -eq 1 ]; then - pnpm --filter @solid-connect/admin run lint - pnpm --filter @solid-connect/admin run format + pnpm --filter @solid-connect/admin run ci:check fi if [ "$RUN_WEB" -eq 0 ] && [ "$RUN_ADMIN" -eq 0 ]; then diff --git a/.husky/pre-push b/.husky/pre-push index 1c24bed2..f49f8c48 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ -echo "πŸ—οΈ Running CI parity builds before push..." +echo "πŸ—οΈ Running CI parity checks before push..." UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name "@{upstream}" 2>/dev/null || true) @@ -33,10 +33,12 @@ for FILE in $CHANGED_FILES; do done if [ "$RUN_WEB" -eq 1 ]; then + pnpm --filter @solid-connect/web run ci:check NODE_ENV=production pnpm --filter @solid-connect/web run build fi if [ "$RUN_ADMIN" -eq 1 ]; then + pnpm --filter @solid-connect/admin run ci:check NODE_ENV=production pnpm --filter @solid-connect/admin run build fi @@ -44,4 +46,4 @@ if [ "$RUN_WEB" -eq 0 ] && [ "$RUN_ADMIN" -eq 0 ]; then echo "ℹ️ No CI-targeted changes detected; skipping parity builds." fi -echo "βœ… CI parity builds passed!" +echo "βœ… CI parity checks passed!" diff --git a/apps/admin/package.json b/apps/admin/package.json index 76c004de..5a51fa7f 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -7,8 +7,9 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run", - "format": "biome format", - "lint": "biome lint", + "format": "biome format --write .", + "format:check": "biome format .", + "lint": "biome check --write .", "lint:check": "biome check .", "typecheck": "tsc --noEmit", "typecheck:ci": "tsc --noEmit", diff --git a/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts b/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts index 57d935d2..23cfcadb 100644 --- a/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts +++ b/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts @@ -1,14 +1,14 @@ import { type RefObject, useEffect, useRef, useState } from "react"; interface UseSectionHandlerReturn { - sectionRef: RefObject; + sectionRef: RefObject; visible: boolean; } const useSectionHandler = (): UseSectionHandlerReturn => { const [visible, setVisible] = useState(false); - const sectionRef = useRef(null); + const sectionRef = useRef(null!); useEffect(() => { if (!sectionRef.current) return; diff --git a/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts b/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts index 4c65cd6f..6aff076b 100644 --- a/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts +++ b/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts @@ -3,14 +3,14 @@ import { type RefObject, useRef, useState } from "react"; import { FilterTab } from "@/types/mentor"; interface UseSelectedTabReturn { - listRef: RefObject; + listRef: RefObject; selectedTab: FilterTab; handleSelectTab: (tab: FilterTab) => void; } const useSelectedTab = (): UseSelectedTabReturn => { const [selectedTab, setSelectedTab] = useState(FilterTab.ALL); - const listRef = useRef(null); + const listRef = useRef(null!); const handleSelectTab = (tab: FilterTab) => { setSelectedTab(tab); diff --git a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx index 0c3815ad..d4483e02 100644 --- a/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx +++ b/apps/web/src/app/mentor/chat/[chatId]/_ui/ChatContent/_ui/ChatMessageBox/index.tsx @@ -29,6 +29,7 @@ const ChatMessageBox = ({ message, currentUserId = 1, partnerNickname = "μƒλŒ€
첨뢀 이미지; + fileInputRef: RefObject; handleImageSelect: () => void; handleFileChange: (event: ChangeEvent) => void; } @@ -22,7 +22,7 @@ const useImageInputHandler = (initImagePreview: string | null): ImageInputHandle }); const [imagePreviewUrl, setImagePreviewUrl] = useState(null); - const fileInputRef = useRef(null); + const fileInputRef = useRef(null!); useEffect(() => { if (initImagePreview) { diff --git a/apps/web/src/app/university/application/ScorePageContent.tsx b/apps/web/src/app/university/application/ScorePageContent.tsx index 63c65956..d7be963f 100644 --- a/apps/web/src/app/university/application/ScorePageContent.tsx +++ b/apps/web/src/app/university/application/ScorePageContent.tsx @@ -22,7 +22,7 @@ interface ScoreData { const ScorePageContent = () => { const router = useRouter(); - const searchRef = useRef(null); + const searchRef = useRef(null!); const [searchActive, setSearchActive] = useState(false); const [preference, setPreference] = useState<"1μˆœμœ„" | "2μˆœμœ„" | "3μˆœμœ„">("1μˆœμœ„"); diff --git a/apps/web/src/app/university/application/ScoreSearchBar.tsx b/apps/web/src/app/university/application/ScoreSearchBar.tsx index f202fc71..2f402fe0 100644 --- a/apps/web/src/app/university/application/ScoreSearchBar.tsx +++ b/apps/web/src/app/university/application/ScoreSearchBar.tsx @@ -3,7 +3,7 @@ import { IconSearchFilled } from "@/public/svgs"; type ScoreSearchBarProps = { onClick: () => void; - textRef: RefObject; + textRef: RefObject; searchHandler: (_e: React.FormEvent) => void; }; diff --git a/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts b/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts index 2b03daf9..f4b87a62 100644 --- a/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts +++ b/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts @@ -5,7 +5,7 @@ import type { ChannelType } from "@/types/mentor"; interface useSelectHandlerReturn { isOpen: boolean; - dropdownRef: RefObject; + dropdownRef: RefObject; handleChannelChange: (val: ChannelType | null) => void; toggleDropdown: () => void; } @@ -21,7 +21,7 @@ const useSelectHandler = ({ onChannelChange, }: UseSelectHandlerProps): useSelectHandlerReturn => { const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); + const dropdownRef = useRef(null!); const { field } = useController({ name, control, defaultValue: null }); diff --git a/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts b/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts index 3eaf36bd..589dea66 100644 --- a/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts +++ b/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts @@ -1,4 +1,4 @@ -import { type MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type MutableRefObject, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; // λ“œλž˜κ·Έ ν•Έλ“€μ—μ„œ μ œμ™Έν•΄μ•Ό ν•˜λŠ” μΈν„°λž™ν‹°λΈŒ μ—˜λ¦¬λ¨ΌνŠΈ νŒλ³„ const isInteractiveElement = (el: EventTarget | null): boolean => { @@ -6,6 +6,7 @@ const isInteractiveElement = (el: EventTarget | null): boolean => { }; interface UseHandleModalReturn { + elementRef: RefObject; isVisible: boolean; translateY: number; isDraggingRef: MutableRefObject; @@ -22,6 +23,7 @@ const useHandleModal = (onClose: () => void, snap: number[] = [0]): UseHandleMod const startYRef = useRef(0); // μ‹œμž‘ Yμ’Œν‘œ const currentYRef = useRef(0); // ν˜„μž¬ Yμ’Œν‘œ const isDraggingRef = useRef(false); // λ“œλž˜κ·Έ μƒνƒœ + const elementRef = useRef(null!); const snapPoints = useMemo((): number[] => { if (typeof window === "undefined") return [0]; // SSR λŒ€μ‘ @@ -113,6 +115,7 @@ const useHandleModal = (onClose: () => void, snap: number[] = [0]): UseHandleMod }, [isVisible, translateY, snapPoints, handleClose]); return { + elementRef, isVisible, translateY, isDraggingRef, diff --git a/apps/web/src/components/ui/FallbackImage.tsx b/apps/web/src/components/ui/FallbackImage.tsx index b2f4c981..552c2f36 100644 --- a/apps/web/src/components/ui/FallbackImage.tsx +++ b/apps/web/src/components/ui/FallbackImage.tsx @@ -4,15 +4,46 @@ import NextImage from "next/image"; import { useState } from "react"; const DEFAULT_FALLBACK_SRC = "/svgs/placeholders/image-placeholder.svg"; +const DEFAULT_CDN_HOST = "https://cdn.default.solid-connection.com"; +const UPLOAD_CDN_HOST = "https://cdn.upload.solid-connection.com"; + +type CdnHostType = "default" | "upload"; + +const CDN_HOSTS: Record = { + default: process.env.NEXT_PUBLIC_IMAGE_URL || DEFAULT_CDN_HOST, + upload: process.env.NEXT_PUBLIC_UPLOADED_IMAGE_URL || UPLOAD_CDN_HOST, +}; + +const resolveCdnUrl = (src: string, cdnHostType?: CdnHostType) => { + const trimmedSrc = src.trim(); + + if (trimmedSrc.length === 0) return ""; + if (trimmedSrc.startsWith("http://") || trimmedSrc.startsWith("https://")) return trimmedSrc; + if (trimmedSrc.startsWith("blob:") || trimmedSrc.startsWith("data:")) return trimmedSrc; + if (trimmedSrc.startsWith("//")) return `https:${trimmedSrc}`; + if (!cdnHostType) return trimmedSrc; + + const normalizedHost = CDN_HOSTS[cdnHostType].replace(/\/+$/, ""); + const normalizedPath = trimmedSrc.replace(/^\/+/, ""); + + return `${normalizedHost}/${normalizedPath}`; +}; type FallbackImageProps = React.ComponentProps & { fallbackSrc?: string; + cdnHostType?: CdnHostType; }; -const FallbackImage = ({ src, fallbackSrc = DEFAULT_FALLBACK_SRC, onError, ...props }: FallbackImageProps) => { +const FallbackImage = ({ + src, + fallbackSrc = DEFAULT_FALLBACK_SRC, + cdnHostType, + onError, + ...props +}: FallbackImageProps) => { const [failedSource, setFailedSource] = useState(null); - const normalizedSrc = typeof src === "string" ? src.trim() || fallbackSrc : src; + const normalizedSrc = typeof src === "string" ? resolveCdnUrl(src, cdnHostType) || fallbackSrc : src; const sourceKey = typeof normalizedSrc === "string" ? normalizedSrc : JSON.stringify(normalizedSrc); const hasError = failedSource === sourceKey; const resolvedSrc = hasError ? fallbackSrc : normalizedSrc; diff --git a/apps/web/src/components/ui/ProfileWithBadge.tsx b/apps/web/src/components/ui/ProfileWithBadge.tsx index c17bf3b5..e92c2b39 100644 --- a/apps/web/src/components/ui/ProfileWithBadge.tsx +++ b/apps/web/src/components/ui/ProfileWithBadge.tsx @@ -1,6 +1,5 @@ import Image from "@/components/ui/FallbackImage"; import { IconDefaultProfile, IconGraduation } from "@/public/svgs/mentor"; -import { convertUploadedImageUrl } from "@/utils/fileUtils"; interface ProfileWithBadgeProps { profileImageUrl?: string | null; @@ -32,7 +31,8 @@ const ProfileWithBadge = ({ {profileImageUrl ? ( ν”„λ‘œν•„ 이미지 run lint`둜 μžλ™ μˆ˜μ • +2. **νƒ€μž… 체크 μ‹€νŒ¨**: TypeScript 였λ₯˜ 직접 μˆ˜μ • +3. **λΉŒλ“œ μ‹€νŒ¨**: λΉŒλ“œ 둜그 확인 ν›„ 였λ₯˜ μˆ˜μ • --- @@ -196,7 +202,7 @@ npm run fix:all # λͺ¨λ“  μžλ™ μˆ˜μ • 적용 git checkout -b feat/new-feature # 2. 개발 μ„œλ²„ μ‹€ν–‰ -npm run dev +pnpm dev # 3. μ½”λ“œ μž‘μ„±... ``` @@ -205,12 +211,12 @@ npm run dev ```bash # μžλ™ μˆ˜μ • 및 검증 -npm run fix:all +pnpm ci:check # λ˜λŠ” κ°œλ³„ μ‹€ν–‰ -npm run lint:fix -npm run format -npm run typecheck +pnpm --filter @solid-connect/web run lint +pnpm --filter @solid-connect/admin run lint +pnpm typecheck ``` ### 3. 컀밋 @@ -261,13 +267,14 @@ git push --force-with-lease origin branch-name ```bash # 1. λ‘œμ»¬μ—μ„œ λ™μΌν•œ 검증 μ‹€ν–‰ -npm run lint:all +pnpm ci:check # 2. μžλ™ μˆ˜μ • μ‹œλ„ -npm run fix:all +pnpm --filter @solid-connect/web run lint +pnpm --filter @solid-connect/admin run lint # 3. λΉŒλ“œ ν…ŒμŠ€νŠΈ -npm run build +pnpm build # 4. μˆ˜μ • ν›„ λ‹€μ‹œ ν‘Έμ‹œ git add . @@ -283,7 +290,7 @@ git push ```bash # 1. Husky μž¬μ„€μΉ˜ -npm run prepare +pnpm run prepare # 2. .husky/commit-msg 파일 확인 cat .husky/commit-msg @@ -301,7 +308,7 @@ chmod +x .husky/commit-msg ```bash # node_modules μ‚­μ œ ν›„ μž¬μ„€μΉ˜ rm -rf node_modules -npm install +pnpm install ``` --- @@ -311,5 +318,4 @@ npm install - [Husky Documentation](https://typicode.github.io/husky/) - [Commitlint Documentation](https://commitlint.js.org/) - [Conventional Commits](https://www.conventionalcommits.org/) -- [ESLint Documentation](https://eslint.org/) -- [Prettier Documentation](https://prettier.io/) +- [Biome Documentation](https://biomejs.dev/) diff --git a/docs/skills/biome-unification-ci-skill.md b/docs/skills/biome-unification-ci-skill.md new file mode 100644 index 00000000..f5142109 --- /dev/null +++ b/docs/skills/biome-unification-ci-skill.md @@ -0,0 +1,59 @@ +# Skill: Biome 단일 ν’ˆμ§ˆμ²΄ν¬ ν‘œμ€€ν™” (Web/Admin/CI) + +## λͺ©μ  + +- λͺ¨λ…Έλ ˆν¬ 전체λ₯Ό Biome 기반 ν’ˆμ§ˆμ²΄ν¬ νλ¦„μœΌλ‘œ ν†΅μΌν•œλ‹€. +- 둜컬(Husky)κ³Ό GitHub Actions의 μ‹€ν–‰ λͺ…령을 λ™μΌν•˜κ²Œ μœ μ§€ν•œλ‹€. + +## ν‘œμ€€ λͺ…λ Ή + +### Web + +- μžλ™ μˆ˜μ •: `pnpm --filter @solid-connect/web run lint` +- 체크 μ „μš©: `pnpm --filter @solid-connect/web run lint:check` +- νƒ€μž… 체크: `pnpm --filter @solid-connect/web run typecheck:ci` +- CI 체크: `pnpm --filter @solid-connect/web run ci:check` + +### Admin + +- μžλ™ μˆ˜μ •: `pnpm --filter @solid-connect/admin run lint` +- 체크 μ „μš©: `pnpm --filter @solid-connect/admin run lint:check` +- νƒ€μž… 체크: `pnpm --filter @solid-connect/admin run typecheck` +- CI 체크: `pnpm --filter @solid-connect/admin run ci:check` + +### Root (Turbo) + +- 전체 CI 체크: `pnpm ci:check` +- 전체 νƒ€μž… 체크: `pnpm typecheck` +- 전체 λΉŒλ“œ: `pnpm build` + +## 적용 원칙 + +1. CIμ—μ„œλŠ” `--write`λ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. +2. μˆ˜λ™ 포맷/린트 μˆ˜μ •μ€ λ‘œμ»¬μ—μ„œλ§Œ μ‹€ν–‰ν•œλ‹€. +3. μ‹ κ·œ νŒ¨ν‚€μ§€ μΆ”κ°€ μ‹œ `lint`, `lint:check`, `typecheck`, `ci:check` 슀크립트λ₯Ό 동일 κ·œμΉ™μœΌλ‘œ λ§žμΆ˜λ‹€. +4. Husky pre-commit/push와 GitHub Actionsκ°€ 같은 슀크립트λ₯Ό ν˜ΈμΆœν•˜λ„λ‘ μœ μ§€ν•œλ‹€. + +## μž‘μ—… 절차 + +1. package 슀크립트 점검 + - `lint`λŠ” μžλ™ μˆ˜μ •, `lint:check`λŠ” 체크 μ „μš©μœΌλ‘œ 뢄리 + - `ci:check`λŠ” `lint:check + typecheck` μ‘°ν•©μœΌλ‘œ κ³ μ • +2. CI μ›Œν¬ν”Œλ‘œμš° 점검 + - 앱별 ν’ˆμ§ˆ λ‹¨κ³„μ—μ„œ `pnpm --filter run ci:check` μ‚¬μš© +3. Husky ν›… 점검 + - pre-commit: λ³€κ²½λœ μ•±μ˜ `ci:check` + - pre-push: λ³€κ²½λœ μ•±μ˜ `ci:check` + `build` + +## 검증 체크리슀트 + +1. `pnpm --filter @solid-connect/web run ci:check` +2. `pnpm --filter @solid-connect/admin run ci:check` +3. `pnpm ci:check` +4. `pnpm build` + +## μ‹€νŒ¨ λŒ€μ‘ + +- Biome 였λ₯˜: 각 μ•±μ—μ„œ `pnpm --filter run lint` ν›„ μž¬μ‹€ν–‰ +- νƒ€μž… 였λ₯˜: ν•΄λ‹Ή μ•± `typecheck` 둜그 κΈ°μ€€μœΌλ‘œ νƒ€μž… μˆ˜μ • +- CI/둜컬 뢈일치: `.husky/*`와 `.github/workflows/ci.yml`의 μ‹€ν–‰ λͺ…령을 같은 κ°’μœΌλ‘œ μž¬μ •λ ¬