Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"check": "biome check"
},
"dependencies": {
"@solid-connect/api-client": "workspace:*",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
Expand Down
16 changes: 1 addition & 15 deletions apps/admin/src/lib/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1 @@
import type { AxiosResponse } from "axios";
import { publicAxiosInstance } from "@/lib/api/client";
import type { AdminSignInResponse, ReissueAccessTokenResponse } from "@/types/auth";

export const adminSignInApi = (email: string, password: string): Promise<AxiosResponse<AdminSignInResponse>> =>
publicAxiosInstance.post("/auth/email/sign-in", { email, password });

export const reissueAccessTokenApi = (refreshToken: string): Promise<AxiosResponse<ReissueAccessTokenResponse>> =>
publicAxiosInstance.post(
"/admin/auth/reissue",
{},
{
headers: { Authorization: `Bearer ${refreshToken}` },
},
);
export { adminSignInApi, reissueAccessTokenApi } from "@solid-connect/api-client/generated/admin";
95 changes: 21 additions & 74 deletions apps/admin/src/lib/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import axios, { type AxiosInstance } from "axios";
import { reissueAccessTokenApi } from "@/lib/api/auth";
import {
axiosInstance,
configureApiClientRuntime,
publicAxiosInstance,
type TokenStorageAdapter,
} from "@solid-connect/api-client/runtime";
import { isTokenExpired } from "@/lib/utils/jwtUtils";
import {
loadAccessToken,
Expand All @@ -9,80 +14,22 @@ import {
saveAccessToken,
} from "@/lib/utils/localStorage";

const convertToBearer = (token: string) => `Bearer ${token}`;
const tokenStorage: TokenStorageAdapter = {
loadAccessToken,
loadRefreshToken,
saveAccessToken,
removeAccessToken,
removeRefreshToken,
};

export const axiosInstance: AxiosInstance = axios.create({
configureApiClientRuntime({
baseURL: import.meta.env.VITE_API_SERVER_URL,
withCredentials: true,
});

axiosInstance.interceptors.request.use(
async (config) => {
const newConfig = { ...config };
let accessToken: string | null = loadAccessToken();

if (accessToken === null || isTokenExpired(accessToken)) {
const refreshToken = loadRefreshToken();
if (refreshToken === null || isTokenExpired(refreshToken)) {
removeAccessToken();
removeRefreshToken();
return config;
}

await reissueAccessTokenApi(refreshToken)
.then((res) => {
accessToken = res.data.accessToken;
saveAccessToken(accessToken);
})
.catch((err) => {
removeAccessToken();
removeRefreshToken();
console.error("인증 토큰 갱신중 오류가 발생했습니다", err);
});
}

if (accessToken !== null) {
newConfig.headers.Authorization = convertToBearer(accessToken);
}
return newConfig;
},
(error) => Promise.reject(error),
);

axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const newError = { ...error };
if (error.response?.status === 401 || error.response?.status === 403) {
const refreshToken = loadRefreshToken();

if (refreshToken === null || isTokenExpired(refreshToken)) {
removeAccessToken();
removeRefreshToken();
throw newError;
}

try {
const newAccessToken = await reissueAccessTokenApi(refreshToken).then((res) => res.data.accessToken);
saveAccessToken(newAccessToken);

if (error?.config.headers === undefined) {
newError.config.headers = {};
}
newError.config.headers.Authorization = convertToBearer(newAccessToken);

return await axios.request(newError.config);
} catch (_err) {
removeAccessToken();
removeRefreshToken();
throw Error("로그인이 필요합니다");
}
} else {
throw newError;
}
tokenStorage,
isTokenExpired,
reissueAccessToken: async (refreshToken: string) => {
const response = await reissueAccessTokenApi(refreshToken);
return response.data.accessToken;
},
);

export const publicAxiosInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_SERVER_URL,
});

export { axiosInstance, publicAxiosInstance };
46 changes: 1 addition & 45 deletions apps/admin/src/lib/api/scores.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1 @@
import { axiosInstance } from "@/lib/api/client";
import type {
GpaScoreUpdateRequest,
GpaScoreWithUser,
LanguageScoreWithUser,
LanguageTestScoreUpdateRequest,
LanguageTestType,
PageResponse,
ScoreSearchCondition,
VerifyStatus,
} from "@/types/scores";

export const scoreApi = {
// GPA 성적 조회
getGpaScores: (condition: ScoreSearchCondition, page: number): Promise<PageResponse<GpaScoreWithUser>> =>
axiosInstance.get("/admin/scores/gpas", { params: { ...condition, page } }).then((res) => res.data),

// GPA 성적 수정
updateGpaScore: (id: number, status: VerifyStatus, reason?: string, score?: GpaScoreWithUser) => {
if (!score) throw new Error("Score data is required");
const request: GpaScoreUpdateRequest = {
gpa: score.gpaScoreStatusResponse.gpaResponse.gpa,
gpaCriteria: score.gpaScoreStatusResponse.gpaResponse.gpaCriteria,
verifyStatus: status,
rejectedReason: reason,
};
return axiosInstance.put(`/admin/scores/gpas/${id}`, request);
},

// 어학성적 조회
getLanguageScores: (condition: ScoreSearchCondition, page: number): Promise<PageResponse<LanguageScoreWithUser>> =>
axiosInstance.get("/admin/scores/language-tests", { params: { ...condition, page } }).then((res) => res.data),

// 어학성적 수정
updateLanguageScore: (id: number, status: VerifyStatus, reason?: string, score?: LanguageScoreWithUser) => {
if (!score) throw new Error("Score data is required");
const request: LanguageTestScoreUpdateRequest = {
languageTestType: score.languageTestScoreStatusResponse.languageTestResponse.languageTestType as LanguageTestType,
languageTestScore: score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore,
verifyStatus: status,
rejectedReason: reason,
};
return axiosInstance.put(`/admin/scores/language-tests/${id}`, request);
},
};
export { scoreApi } from "@solid-connect/api-client/generated/admin";
9 changes: 1 addition & 8 deletions apps/admin/src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1 @@
export interface AdminSignInResponse {
accessToken: string;
refreshToken: string;
}

export interface ReissueAccessTokenResponse {
accessToken: string;
}
export type { AdminSignInResponse, ReissueAccessTokenResponse } from "@solid-connect/api-client/generated/admin";
118 changes: 13 additions & 105 deletions apps/admin/src/types/scores.ts
Original file line number Diff line number Diff line change
@@ -1,105 +1,13 @@
export type VerifyStatus = "PENDING" | "APPROVED" | "REJECTED";

export interface ScoreSearchCondition {
verifyStatus?: VerifyStatus;
}

export interface GpaResponse {
gpa: number;
gpaCriteria: number;
gpaReportUrl: string;
}

export interface GpaScore {
verifyStatus: VerifyStatus;
rejectedReason?: string;
}

export interface GpaScoreStatusResponse {
id: number;
gpaResponse: GpaResponse;
verifyStatus: VerifyStatus;
rejectedReason: string | null;
createdAt: string;
updatedAt: string;
}

export interface SiteUserResponse {
id: number;
nickname: string;
profileImageUrl: string;
}

export interface GpaScoreWithUser {
gpaScoreStatusResponse: GpaScoreStatusResponse;
siteUserResponse: SiteUserResponse;
}

export interface PageResponse<T> {
content: T[];
pageNumber: number;
pageSize: number;
totalElements: number;
totalPages: number;
}

export interface LanguageResponse {
languageType: string;
score: number;
testDate: string;
expireDate: string;
languageReportUrl: string;
}

export interface LanguageTestResponse {
languageTestType: string;
languageTestScore: string;
languageTestReportUrl: string;
}

export interface LanguageTestScore {
verifyStatus: VerifyStatus;
rejectedReason?: string;
}

export interface LanguageTestScoreStatusResponse {
id: number;
languageTestResponse: LanguageTestResponse;
verifyStatus: VerifyStatus;
rejectedReason: string | null;
createdAt: string;
updatedAt: string;
}

export interface LanguageScoreWithUser {
languageTestScoreStatusResponse: LanguageTestScoreStatusResponse;
siteUserResponse: SiteUserResponse;
}

export type LanguageTestType =
| "TOEIC"
| "TOEFL_IBT"
| "TOEFL_ITP"
| "IELTS"
| "JLPT"
| "NEW_HSK"
| "ETC"
| "DALF"
| "CEFR"
| "TCF"
| "TEF"
| "DUOLINGO";

export interface GpaScoreUpdateRequest {
gpa: number;
gpaCriteria: number;
verifyStatus: VerifyStatus;
rejectedReason?: string;
}

export interface LanguageTestScoreUpdateRequest {
languageTestType: LanguageTestType;
languageTestScore: string;
verifyStatus: VerifyStatus;
rejectedReason?: string;
}
export type {
GpaScoreStatusResponse,
GpaScoreUpdateRequest,
GpaScoreWithUser,
LanguageScoreWithUser,
LanguageTestScoreStatusResponse,
LanguageTestScoreUpdateRequest,
LanguageTestType,
PageResponse,
ScoreSearchCondition,
SiteUserResponse,
VerifyStatus,
} from "@solid-connect/api-client/generated/admin";
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
"dev": "turbo dev",
"build": "turbo build",
"sync:bruno": "turbo run sync:bruno",
"codegen:check": "turbo run codegen:check",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
"ci:check": "turbo ci:check",
"prepare": "husky"
},
"pnpm": {
"onlyBuiltDependencies": ["bruno-api-typescript"]
"onlyBuiltDependencies": [
"bruno-api-typescript"
]
},
"devDependencies": {
"@biomejs/biome": "^2.3.11",
Expand Down
24 changes: 24 additions & 0 deletions packages/api-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@solid-connect/api-client",
"version": "0.0.0",
"private": true,
"scripts": {
"sync:bruno": "node ./scripts/sync-bruno.mjs",
"sync:bruno:remote": "BRUNO_SOURCE_MODE=remote node ./scripts/sync-bruno.mjs",
"build": "pnpm run sync:bruno",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check turbo.json build and ci:check configuration
echo "=== turbo.json build task ==="
jq '.tasks.build' turbo.json 2>/dev/null || cat turbo.json | grep -A 10 '"build"'

echo ""
echo "=== turbo.json ci:check task ==="
jq '.tasks["ci:check"]' turbo.json 2>/dev/null || cat turbo.json | grep -A 10 '"ci:check"'

echo ""
echo "=== Check sync:bruno task config ==="
jq '.tasks["sync:bruno"]' turbo.json 2>/dev/null || true

echo ""
echo "=== List CI workflow files ==="
fd -e yml -e yaml . .github/workflows 2>/dev/null | head -20

Repository: solid-connection/solid-connect-web

Length of output: 763


🏁 Script executed:

#!/bin/bash
echo "=== CI workflow configuration ==="
cat .github/workflows/ci.yml | head -100

echo ""
echo "=== Search for turbo commands in CI workflow ==="
grep -n "turbo\|pnpm.*build\|pnpm.*check" .github/workflows/ci.yml || echo "No turbo/pnpm commands found in snippet"

Repository: solid-connection/solid-connect-web

Length of output: 2849


🏁 Script executed:

#!/bin/bash
echo "=== Full CI workflow file ==="
wc -l .github/workflows/ci.yml

echo ""
echo "=== Search for codegen references in CI workflow ==="
grep -n "codegen" .github/workflows/ci.yml || echo "No codegen references found"

echo ""
echo "=== api-client package.json exports and build info ==="
cat packages/api-client/package.json | jq '{name, scripts, exports, type, main}'

echo ""
echo "=== Check api-client's actual output structure ==="
ls -la packages/api-client/src/ 2>/dev/null | head -20 || echo "No src directory visible"

Repository: solid-connection/solid-connect-web

Length of output: 1351


codegen:check가 CI 파이프라인에 포함되지 않고 있습니다.

원래 리뷰 의견이 가정했던 상황과 실제가 다릅니다. 현재 설계의 핵심:

  1. 빌드 중 생성 파일 갱신: "build": "pnpm run sync:bruno" 설정은 의도된 것입니다. Admin 앱 빌드 시 turbo의 ^build 의존성으로 인해 api-client가 빌드되며, 항상 최신 API 클라이언트를 생성합니다.

  2. 캐시 비활성화: sync:bruno 태스크에 cache: false가 설정되어 있어서 turbo 캐시를 무시하고 매번 실행됩니다. 이는 빌드 시점에 항상 신선한 생성 파일을 보장하려는 의도입니다.

  3. 누락된 검증: 문제는 codegen:check 스크립트가 존재하지만 CI 파이프라인에 포함되지 않았다는 점입니다. 현재 CI는 ci:check (lint & typecheck만)만 실행하고, 생성된 파일이 소스와 일치하는지 검증하지 않습니다.

권장사항:

  • ci:check 태스크에 codegen:check 의존성을 추가하여 CI에서 생성 파일의 동기화 상태를 검증할 것
  • 또는 별도 CI 단계에서 codegen:check를 실행하여 개발자가 로컬에서 sync:bruno를 실행하지 않은 채 커밋한 경우를 감지할 것
🤖 Prompt for AI Agents
In `@packages/api-client/package.json` at line 8, CI is not validating that
generated API client files are up-to-date because the package.json scripts run
"sync:bruno" only during build but do not include "codegen:check" in the CI
checks; update the project's npm scripts so that the CI validation step (the
"ci:check" script) depends on or invokes "codegen:check" (or add a dedicated CI
job that runs "codegen:check") to ensure the generated client from "sync:bruno"
is verified during CI; modify the "ci:check" script to include "codegen:check"
(or add a new pipeline stage that runs "pnpm codegen:check") so any out-of-sync
generated files are caught.

"typecheck": "tsc --noEmit",
"codegen:check": "pnpm run sync:bruno && git diff --exit-code src/generated/apis || (echo \"Generated API client is out of sync. Run: pnpm --filter @solid-connect/api-client run sync:bruno\" && exit 1)"
},
"exports": {
".": "./src/index.ts",
"./runtime": "./src/runtime/index.ts",
"./generated/admin": "./src/generated/admin/index.ts",
"./hooks": "./src/hooks/index.ts"
},
"dependencies": {
"axios": "^1.6.7"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
Loading
Loading