Skip to content

[FEAT/#119] 담당 페이지 API 연동#120

Open
taegeon2 wants to merge 1 commit into
feat/#116-student-dashboard-api-syncfrom
feat/#119-assigned-pages-api-sync
Open

[FEAT/#119] 담당 페이지 API 연동#120
taegeon2 wants to merge 1 commit into
feat/#116-student-dashboard-api-syncfrom
feat/#119-assigned-pages-api-sync

Conversation

@taegeon2

Copy link
Copy Markdown
Contributor

📝 요약

  • 담당 페이지의 미연동 API를 Swagger 명세 기준으로 연결했습니다.

작업 내용

  • 신고, 리뷰, 관리자 대시보드/건의함, 제휴업체 순위/리뷰, 맵 검색/주변 가게 API를 연동했습니다.
  • 로딩/에러/빈 상태 처리와 개발 환경용 요청/응답 로그를 추가했습니다.

🔗 관련 이슈

체크리스트

  • 코딩 컨벤션(Biome/Lint)을 준수하였습니다.
  • 모든 타입 에러를 해결하였습니다. (Typecheck)
  • 변경 사항에 대한 테스트를 마쳤습니다.
  • 불필요한 로그(console.log)를 제거하였습니다.

💬 리뷰어에게

  • 현재 EXPO_PUBLIC_API_BASE_URL이 비어 있고 assu.shop Swagger 접속이 timeout되어 실제 서버 응답 검증은 정확히 하지 못했습니다.
  • API 확인을 위해 __DEV__ 조건부 요청/응답 로그를 남겨두었습니다.

@taegeon2 taegeon2 self-assigned this Jun 28, 2026
@github-actions

Copy link
Copy Markdown

Thanks for the contribution!
I have applied any labels matching special text in your title and description.

Please review the labels and make any necessary changes.

@github-actions github-actions Bot added the feature 새로운 기능 구현 label Jun 28, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request integrates real API endpoints across student, partner, and admin dashboards, replacing previous mock implementations. Key changes include integrating report creation, review management, admin dashboard statistics, partnership suggestions, partner weekly rankings, and dynamic KakaoMap store markers. The review feedback focuses on adding defensive programming checks across various API adapters and hooks to prevent runtime crashes from missing or null response fields, as well as optimizing the KakaoMap component to avoid redundant marker updates when the array reference changes but the data remains identical.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 46 to 48
const appKey = process.env.EXPO_PUBLIC_KAKAO_JS_KEY ?? "";
const webViewRef = useRef<WebView>(null);
const [isMapReady, setIsMapReady] = useState(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

KakaoMap 컴포넌트가 렌더링될 때마다 markers 배열의 참조가 변경되어 불필요한 injectJavaScript 호출이 발생할 수 있습니다. 이전 마커 상태를 기억하기 위해 prevMarkersRef를 추가합니다.

Suggested change
const appKey = process.env.EXPO_PUBLIC_KAKAO_JS_KEY ?? "";
const webViewRef = useRef<WebView>(null);
const [isMapReady, setIsMapReady] = useState(false);
const appKey = process.env.EXPO_PUBLIC_KAKAO_JS_KEY ?? "";
const webViewRef = useRef<WebView>(null);
const prevMarkersRef = useRef<string>("");
const [isMapReady, setIsMapReady] = useState(false);

Comment on lines +84 to +93
useEffect(() => {
if (!isMapReady) return;
const serializedMarkers = JSON.stringify(markers).replace(
/</g,
"\\u003c",
);
webViewRef.current?.injectJavaScript(
`window.updateStoreMarkers(${serializedMarkers}); true;`,
);
}, [isMapReady, markers]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

markers 배열의 참조가 변경되더라도 실제 데이터 내용이 동일하다면 WebView에 마커를 다시 그리는 무거운 작업을 방지해야 합니다. prevMarkersRef를 사용하여 문자열화된 마커 데이터를 비교하고, 변경된 경우에만 injectJavaScript를 호출하도록 최적화합니다.

		useEffect(() => {
			if (!isMapReady) return;
			const serialized = JSON.stringify(markers).replace(/</g, "\\u003c");
			if (prevMarkersRef.current === serialized) return;
			prevMarkersRef.current = serialized;
			webViewRef.current?.injectJavaScript(
				`window.updateStoreMarkers(${serialized}); true;`,
			);
		}, [isMapReady, markers]);

Comment on lines +10 to +43
export function toAdminDashboardData({
auth,
newCount,
todayUsage,
topUsage,
usageList,
}: {
auth: CountAdminAuthResponseDto;
newCount: NewCountAdminResponseDto;
todayUsage: CountUsagePersonResponseDto;
topUsage: CountUsageResponseDto;
usageList: CountUsageListResponseDto;
}): DashboardData {
const monthlyUsage: MonthlyUsage[] = usageList.items
.slice(0, 6)
.map((item) => ({
month: item.storeName,
count: item.usageCount,
}));

return {
adminName: auth.adminName,
stats: {
appCertified: auth.studentCount,
newJoined: newCount.newStudentCount,
affiliateUsers: todayUsage.usagePersonCount,
},
monthlyUsage,
insight: {
topStoreName: topUsage.storeName,
expectedCount: topUsage.usageCount,
},
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

API 응답 데이터가 비어있거나(null 또는 undefined) 특정 필드가 누락된 경우를 대비해 방어적 코드(Defensive Programming)를 적용하는 것이 안전합니다. 특히 topUsagenull이거나 usageList.items가 없을 때 발생할 수 있는 런타임 에러를 방지합니다.

export function toAdminDashboardData({
	auth,
	newCount,
	todayUsage,
	topUsage,
	usageList,
}: {
	auth: CountAdminAuthResponseDto;
	newCount: NewCountAdminResponseDto;
	todayUsage: CountUsagePersonResponseDto;
	topUsage: CountUsageResponseDto | null;
	usageList: CountUsageListResponseDto;
}): DashboardData {
	const monthlyUsage: MonthlyUsage[] = (usageList?.items ?? [])
		.slice(0, 6)
		.map((item) => ({
			month: item.storeName,
			count: item.usageCount,
		}));

	return {
		adminName: auth?.adminName ?? "",
		stats: {
			appCertified: auth?.studentCount ?? 0,
			newJoined: newCount?.newStudentCount ?? 0,
			affiliateUsers: todayUsage?.usagePersonCount ?? 0,
		},
		monthlyUsage,
		insight: {
			topStoreName: topUsage?.storeName ?? null,
			expectedCount: topUsage?.usageCount ?? 0,
		},
	};
}

Comment on lines +22 to +39
function toReadableEnum(value: string): string {
return value
.split("_")
.map((word) => word.slice(0, 1) + word.slice(1).toLowerCase())
.join(" ");
}

export function toSuggestion(dto: GetSuggestionResponseDto): Suggestion {
return {
id: String(dto.suggestionId),
storeName: dto.storeName,
department:
MAJOR_LABELS[dto.studentMajor] ?? toReadableEnum(dto.studentMajor),
studentStatus: STATUS_LABELS[dto.enrollmentStatus] ?? dto.enrollmentStatus,
content: dto.content,
createdAt: new Date(dto.createdAt),
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

dto.studentMajordto.enrollmentStatus 등 API 응답 필드가 누락되거나 null인 경우 toReadableEnum 내부에서 split 호출 시 런타임 에러가 발생할 수 있습니다. 안전한 기본값 제공 및 방어적 코드를 적용합니다.

Suggested change
function toReadableEnum(value: string): string {
return value
.split("_")
.map((word) => word.slice(0, 1) + word.slice(1).toLowerCase())
.join(" ");
}
export function toSuggestion(dto: GetSuggestionResponseDto): Suggestion {
return {
id: String(dto.suggestionId),
storeName: dto.storeName,
department:
MAJOR_LABELS[dto.studentMajor] ?? toReadableEnum(dto.studentMajor),
studentStatus: STATUS_LABELS[dto.enrollmentStatus] ?? dto.enrollmentStatus,
content: dto.content,
createdAt: new Date(dto.createdAt),
};
}
function toReadableEnum(value: string): string {
if (!value) return "";
return value
.split("_")
.map((word) => word.slice(0, 1) + word.slice(1).toLowerCase())
.join(" ");
}
export function toSuggestion(dto: GetSuggestionResponseDto): Suggestion {
return {
id: String(dto.suggestionId),
storeName: dto.storeName ?? "",
department:
MAJOR_LABELS[dto.studentMajor] ?? toReadableEnum(dto.studentMajor ?? ""),
studentStatus: STATUS_LABELS[dto.enrollmentStatus] ?? dto.enrollmentStatus ?? "",
content: dto.content ?? "",
createdAt: dto.createdAt ? new Date(dto.createdAt) : new Date(),
};
}

Comment on lines +15 to +19
return res.data.result.map((item, index) => ({
weekLabel: `${index + 1}주차`,
rank: item.rank,
usageCount: item.usageCount,
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

서버로부터 주간 순위 데이터(res.data.result)가 없거나 null로 반환될 경우 .map() 호출 시 런타임 에러가 발생할 수 있습니다. 안전하게 빈 배열(?? [])을 기본값으로 사용하도록 수정합니다.

Suggested change
return res.data.result.map((item, index) => ({
weekLabel: `${index + 1}주차`,
rank: item.rank,
usageCount: item.usageCount,
}));
return (res.data.result ?? []).map((item, index) => ({
weekLabel: `${index + 1}주차`,
rank: item.rank,
usageCount: item.usageCount,
}));

Comment on lines +52 to +59
return {
reviews: page.content.map(toReview),
totalElements: page.totalElements,
totalPages: page.totalPages,
page: page.number,
size: page.size,
isLast: page.last,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

API 응답의 result 또는 content가 누락되었을 때 발생할 수 있는 런타임 에러를 방지하기 위해 옵셔널 체이닝과 기본값을 적용하여 방어적으로 처리합니다.

Suggested change
return {
reviews: page.content.map(toReview),
totalElements: page.totalElements,
totalPages: page.totalPages,
page: page.number,
size: page.size,
isLast: page.last,
};
return {
reviews: (page?.content ?? []).map(toReview),
totalElements: page?.totalElements ?? 0,
totalPages: page?.totalPages ?? 0,
page: page?.number ?? 0,
size: page?.size ?? 0,
isLast: page?.last ?? true,
};

Comment on lines +224 to +236
const stores =
storeSearchResult.status === "fulfilled"
? pickList(storeSearchResult.value.data.result)
.map(toSearchResultStore)
.filter((store): store is SearchResultStore => store !== null)
: [];
const places =
placeSearchResult.status === "fulfilled"
? (Array.isArray(placeSearchResult.value.data.result)
? placeSearchResult.value.data.result
: []
).map(toPlaceSearchResult)
: [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

API 요청이 성공하더라도 value.data가 비어있을 경우 value.data.result 접근 시 에러가 발생할 수 있으므로 옵셔널 체이닝(data?.result)을 적용하는 것이 안전합니다.

Suggested change
const stores =
storeSearchResult.status === "fulfilled"
? pickList(storeSearchResult.value.data.result)
.map(toSearchResultStore)
.filter((store): store is SearchResultStore => store !== null)
: [];
const places =
placeSearchResult.status === "fulfilled"
? (Array.isArray(placeSearchResult.value.data.result)
? placeSearchResult.value.data.result
: []
).map(toPlaceSearchResult)
: [];
const stores =
storeSearchResult.status === "fulfilled"
? pickList(storeSearchResult.value.data?.result)
.map(toSearchResultStore)
.filter((store): store is SearchResultStore => store !== null)
: [];
const places =
placeSearchResult.status === "fulfilled"
? (Array.isArray(placeSearchResult.value.data?.result)
? placeSearchResult.value.data.result
: []
).map(toPlaceSearchResult)
: [];

@eunhyekimyeah eunhyekimyeah left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

수고하셨어욥~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants