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
3 changes: 1 addition & 2 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -33,15 +33,17 @@ 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

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!"
Comment on lines 45 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

pre-commit과 동일한 문제: 체크 미실행 시에도 성공 메시지가 출력됩니다.

Line 46에서 "skipping parity builds"를 출력하고, Line 49에서 "CI parity checks passed!"도 출력됩니다. pre-commit 훅과 동일하게 조건부 처리를 권장합니다.

🔧 제안
 if [ "$RUN_WEB" -eq 0 ] && [ "$RUN_ADMIN" -eq 0 ]; then
   echo "ℹ️ No CI-targeted changes detected; skipping parity builds."
+else
+  echo "✅ CI parity checks passed!"
 fi
-
-echo "✅ CI parity checks passed!"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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!"
if [ "$RUN_WEB" -eq 0 ] && [ "$RUN_ADMIN" -eq 0 ]; then
echo "ℹ️ No CI-targeted changes detected; skipping parity builds."
else
echo "✅ CI parity checks passed!"
fi
🤖 Prompt for AI Agents
In @.husky/pre-push around lines 45 - 49, The current pre-push script prints "CI
parity checks passed!" unconditionally even when both RUN_WEB and RUN_ADMIN are
0; update the conditional so the success message is only printed when parity
checks actually ran. Concretely, adjust the if/else around the existing block
that checks RUN_WEB and RUN_ADMIN (the shell variables RUN_WEB and RUN_ADMIN and
the echo "ℹ️ No CI-targeted changes detected; skipping parity builds.") so that
the echo "✅ CI parity checks passed!" is emitted in the branch where checks were
executed (i.e., when either RUN_WEB or RUN_ADMIN is non-zero), mirroring the
conditional behavior used in the pre-commit hook.

5 changes: 3 additions & 2 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { type RefObject, useEffect, useRef, useState } from "react";

interface UseSectionHandlerReturn {
sectionRef: RefObject<HTMLDivElement | null>;
sectionRef: RefObject<HTMLDivElement>;
visible: boolean;
}

const useSectionHandler = (): UseSectionHandlerReturn => {
const [visible, setVisible] = useState<boolean>(false);

const sectionRef = useRef<HTMLDivElement | null>(null);
const sectionRef = useRef<HTMLDivElement>(null!);
useEffect(() => {
if (!sectionRef.current) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { type RefObject, useRef, useState } from "react";
import { FilterTab } from "@/types/mentor";

interface UseSelectedTabReturn {
listRef: RefObject<HTMLDivElement | null>;
listRef: RefObject<HTMLDivElement>;
selectedTab: FilterTab;
handleSelectTab: (tab: FilterTab) => void;
}

const useSelectedTab = (): UseSelectedTabReturn => {
const [selectedTab, setSelectedTab] = useState<FilterTab>(FilterTab.ALL);
const listRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null!);

const handleSelectTab = (tab: FilterTab) => {
setSelectedTab(tab);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ChatMessageBox = ({ message, currentUserId = 1, partnerNickname = "상대
<div className="relative overflow-hidden rounded-lg">
<Image
src={attachment.url}
cdnHostType="upload"
alt="첨부 이미지"
width={200}
height={150}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { convertUploadedImageUrl } from "@/utils/fileUtils";
interface ImageInputHandlerReturn {
selectedImage: File | undefined;
imagePreviewUrl: string | null;
fileInputRef: RefObject<HTMLInputElement | null>;
fileInputRef: RefObject<HTMLInputElement>;
handleImageSelect: () => void;
handleFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
}
Expand All @@ -22,7 +22,7 @@ const useImageInputHandler = (initImagePreview: string | null): ImageInputHandle
});

const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null!);

useEffect(() => {
if (initImagePreview) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface ScoreData {

const ScorePageContent = () => {
const router = useRouter();
const searchRef = useRef<HTMLInputElement | null>(null);
const searchRef = useRef<HTMLInputElement>(null!);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# apps/web/package.json에서 React와 `@types/react` 버전 확인
find apps/web -maxdepth 2 -name "package.json" -type f -exec sh -c 'echo "=== {} ===" && cat "$1" | jq -r ".dependencies.react, .devDependencies[\"@types/react\"], .dependencies[\"react\"]" ' _ {} \;

Repository: solid-connection/solid-connect-web

Length of output: 123


🏁 Script executed:

# apps/web/COMPONENTS.md 확인
cat apps/web/COMPONENTS.md 2>/dev/null || echo "File not found"

Repository: solid-connection/solid-connect-web

Length of output: 9064


🏁 Script executed:

# ScorePageContent.tsx의 searchRef 사용 전체 확인
sed -n '20,160p' apps/web/src/app/university/application/ScorePageContent.tsx | cat -n

Repository: solid-connection/solid-connect-web

Length of output: 6029


React 버전 불일치 해결이 필요합니다.

현재 프로젝트의 의존성 구성에 다음과 같은 버전 불일치가 있습니다:

  1. Runtime과 Type 정의 버전 불일치

    • React 실행 환경: ^18 (React 18.x)
    • React 타입 정의: 19.2.10 (@types/react 19.x)
  2. null! 패턴의 원인

    • useRef<HTMLInputElement>(null!)의 non-null assertion은 React 18 타입과 React 19 타입의 불일치를 우회하기 위한 임시 해결책입니다.
    • React 19 타입에서는 RefObject<T>의 타입 정의가 변경되어 이러한 workaround가 필요합니다.
  3. 권장 해결 방안

    • @types/react를 18.x로 downgrade하여 런타임 버전과 일치시키거나
    • React를 19.x로 upgrade하여 타입 정의와 일치시키세요.
    • 현재 코드는 optional chaining과 null check로 안전하게 동작하지만, 타입 시스템의 일관성을 위해 버전 정렬이 필요합니다.
🤖 Prompt for AI Agents
In `@apps/web/src/app/university/application/ScorePageContent.tsx` at line 25, The
non-null assertion on searchRef (const searchRef =
useRef<HTMLInputElement>(null!)) is a workaround for a React/@types version
mismatch; fix by aligning React runtime and types (either downgrade `@types/react`
to 18.x or upgrade react/react-dom to 19.x) and make the ref type-safe: change
the ref declaration to useRef<HTMLInputElement | null>(null) in ScorePageContent
(symbol: searchRef) and update any usages to handle possible null (optional
chaining or null checks) instead of relying on null!.


const [searchActive, setSearchActive] = useState(false);
const [preference, setPreference] = useState<"1순위" | "2순위" | "3순위">("1순위");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IconSearchFilled } from "@/public/svgs";

type ScoreSearchBarProps = {
onClick: () => void;
textRef: RefObject<HTMLInputElement | null>;
textRef: RefObject<HTMLInputElement>;
searchHandler: (_e: React.FormEvent) => void;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ChannelType } from "@/types/mentor";

interface useSelectHandlerReturn {
isOpen: boolean;
dropdownRef: RefObject<HTMLDivElement | null>;
dropdownRef: RefObject<HTMLDivElement>;
handleChannelChange: (val: ChannelType | null) => void;
toggleDropdown: () => void;
}
Expand All @@ -21,7 +21,7 @@ const useSelectHandler = ({
onChannelChange,
}: UseSelectHandlerProps): useSelectHandlerReturn => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null!);

const { field } = useController({ name, control, defaultValue: null });

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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 => {
return el instanceof HTMLElement && ["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A", "LABEL"].includes(el.tagName);
};

interface UseHandleModalReturn {
elementRef: RefObject<HTMLDivElement>;
isVisible: boolean;
translateY: number;
isDraggingRef: MutableRefObject<boolean>;
Expand All @@ -22,6 +23,7 @@ const useHandleModal = (onClose: () => void, snap: number[] = [0]): UseHandleMod
const startYRef = useRef<number>(0); // 시작 Y좌표
const currentYRef = useRef<number>(0); // 현재 Y좌표
const isDraggingRef = useRef<boolean>(false); // 드래그 상태
const elementRef = useRef<HTMLDivElement>(null!);

const snapPoints = useMemo((): number[] => {
if (typeof window === "undefined") return [0]; // SSR 대응
Expand Down Expand Up @@ -113,6 +115,7 @@ const useHandleModal = (onClose: () => void, snap: number[] = [0]): UseHandleMod
}, [isVisible, translateY, snapPoints, handleClose]);

return {
elementRef,
isVisible,
translateY,
isDraggingRef,
Expand Down
35 changes: 33 additions & 2 deletions apps/web/src/components/ui/FallbackImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CdnHostType, string> = {
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<typeof NextImage> & {
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<string | null>(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;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/ui/ProfileWithBadge.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -32,7 +31,8 @@ const ProfileWithBadge = ({
{profileImageUrl ? (
<Image
unoptimized
src={convertUploadedImageUrl(profileImageUrl)}
src={profileImageUrl}
cdnHostType="upload"
alt="프로필 이미지"
width={width}
height={height}
Expand Down
Loading