Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Expand Up @@ -6,6 +6,7 @@ const isInteractiveElement = (el: EventTarget | null): boolean => {
};

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
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
Loading