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
1 change: 1 addition & 0 deletions apps/web/src/core/shared/util/date/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
type RelativeTimeOptions,
} from "./format-relative-time";
export { formatTime, formatTimeRange } from "./format-time";
export { parseHour, parseMinute, parseSecond } from "./parse-time";
59 changes: 59 additions & 0 deletions apps/web/src/core/shared/util/date/parse-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Time Parsing Utilities
*
* 시간 문자열 파싱 유틸리티 함수들
*/

/**
* 시간 문자열에서 시(hour) 파싱
*
* @param hourStr - 시간 문자열 (예: "11", "09")
* @returns 0-23 범위의 숫자, 유효하지 않으면 null
*
* @example
* parseHour("11") // 11
* parseHour("09") // 9
* parseHour("25") // null (범위 초과)
* parseHour("abc") // null (숫자 아님)
*/
export function parseHour(hourStr: string): number | null {
const hour = Number.parseInt(hourStr, 10);
if (Number.isNaN(hour) || hour < 0 || hour > 23) return null;
return hour;
}

/**
* 시간 문자열에서 분(minute) 파싱
*
* @param minuteStr - 분 문자열 (예: "30", "05")
* @returns 0-59 범위의 숫자, 유효하지 않으면 null
*
* @example
* parseMinute("30") // 30
* parseMinute("05") // 5
* parseMinute("60") // null (범위 초과)
*/
export function parseMinute(minuteStr: string): number | null {
const minute = Number.parseInt(minuteStr, 10);
if (Number.isNaN(minute) || minute < 0 || minute > 59) return null;
return minute;
}

/**
* 시간 문자열에서 초(second) 파싱
*
* @param secondStr - 초 문자열 (선택적, 예: "00", "45")
* @returns 0-59 범위의 숫자, 없으면 0, 유효하지 않으면 0
*
* @example
* parseSecond("45") // 45
* parseSecond("00") // 0
* parseSecond(undefined) // 0
* parseSecond("60") // 0 (범위 초과)
*/
export function parseSecond(secondStr: string | undefined): number {
if (!secondStr) return 0;
const second = Number.parseInt(secondStr, 10);
if (Number.isNaN(second) || second < 0 || second > 59) return 0;
return second;
}
29 changes: 17 additions & 12 deletions apps/web/src/domains/cafeteria/data/mapper/cafeteria.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import { PageInfoSchema } from "@core/types";
import { parseHour, parseMinute, parseSecond } from "@core/utils/date";
// Import Entity classes from Domain Layer
import {
type BusinessHours,
Expand Down Expand Up @@ -58,19 +59,23 @@ export function businessHoursDtoToDomain(

// Helper to validate and convert LocalTime DTO
const convertLocalTime = (time: any) => {
if (
!time ||
typeof time.hour !== "number" ||
typeof time.minute !== "number"
) {
return null;
if (!time) return null;

// 문자열 형식 처리 ("11:30:00" 또는 "11:30")
if (typeof time === "string") {
const parts = time.split(":");
if (parts.length < 2) return null;

const hour = parseHour(parts[0]);
const minute = parseMinute(parts[1]);
const second = parseSecond(parts[2]);

if (hour === null || minute === null) return null;

return { hour, minute, second, nano: 0 };
}
return {
hour: time.hour,
minute: time.minute,
second: time.second ?? 0,
nano: time.nano ?? 0,
};

return null;
};

// Helper to convert TimeRange DTO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
SuccessResponseRegisterCafeteriaMenuResponse,
SuccessResponseRegisterCafeteriaResponse,
} from "../dto";
import { CafeteriaRemoteDataSourceMockImpl } from "./cafeteria-remote-data-source-mock-impl";

export class CafeteriaRemoteDataSourceImpl
implements CafeteriaRemoteDataSource
Expand Down Expand Up @@ -109,7 +110,7 @@ export class CafeteriaRemoteDataSourceImpl
* 구내식당 메뉴 타임라인 조회 (무한 스크롤)
*
* TODO: 백엔드 API 완성되면 실제 엔드포인트로 교체
* TEMPORARY: 현재는 stub 구현에서만 사용됨
* TEMPORARY: Mock 데이터 사용
*/
async getCafeteriaMenuTimeline(
_id: string,
Expand All @@ -121,16 +122,19 @@ export class CafeteriaRemoteDataSourceImpl
data: GetCafeteriaMenuTimelineResponse[];
pageInfo: PageInfo;
}> {
// TEMPORARY: 백엔드 API 준비 전까지 Mock 데이터 사용
const mockImpl = new CafeteriaRemoteDataSourceMockImpl();
return mockImpl.getCafeteriaMenuTimeline();
// TODO: 실제 API 구현 시 아래 코드로 교체
// const response = await this.httpClient.get<PageResponse...>(
// `/api/v1/cafeterias/${id}/menus/timeline`,
// { params }
// );
// return { data: response.data.data || [], pageInfo: response.data.pageInfo || ... };

throw new Error(
"getCafeteriaMenuTimeline is not yet implemented in real API - use stub repository",
);
// throw new Error(
// "getCafeteriaMenuTimeline is not yet implemented in real API - use stub repository",
// );
}

/**
Expand Down
155 changes: 68 additions & 87 deletions apps/web/src/domains/cafeteria/di/cafeteria-client-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

// Data Layer
import {
CafeteriaRemoteDataSourceMockImpl,
CafeteriaRemoteDataSourceImpl,
CafeteriaRepositoryImpl,
CafeteriaReviewRemoteDataSourceMockImpl,
CafeteriaReviewRepositoryImpl,
Expand Down Expand Up @@ -34,6 +34,12 @@ import {
type RegisterCafeteriaUseCase,
RegisterCafeteriaUseCaseImpl,
} from "@cafeteria/domain";
// Infrastructure Layer
import { AuthenticatedHttpClient } from "@core/infrastructure/http/authenticated-http-client";
import { ClientTokenProvider } from "@core/infrastructure/http/client-token-provider";
import { FetchHttpClient } from "@core/infrastructure/http/fetch-http-client";
import type { HttpClient } from "@core/infrastructure/http/http-client.interface";
import { ClientSessionManager } from "@core/infrastructure/storage/client-session-manager";

export interface CafeteriaClientContainer {
// Cafeteria UseCase Getters
Expand All @@ -52,31 +58,56 @@ export interface CafeteriaClientContainer {
}

class CafeteriaClientContainerImpl implements CafeteriaClientContainer {
// Data Layer (Lazy)
private _cafeteriaDataSource?: CafeteriaRemoteDataSourceMockImpl;
private _sessionManager?: ClientSessionManager;
private _tokenProvider?: ClientTokenProvider;
private _baseClient?: FetchHttpClient;
private _httpClient?: HttpClient;

private _cafeteriaDataSource?: CafeteriaRemoteDataSourceImpl;
private _cafeteriaRepository?: CafeteriaRepositoryImpl;
private _reviewDataSource?: CafeteriaReviewRemoteDataSourceMockImpl;
private _reviewRepository?: CafeteriaReviewRepositoryImpl;

// Cafeteria UseCases (Lazy)
private _getCafeteriasWithMenuUseCase?: GetCafeteriasWithMenuUseCase;
private _getCafeteriaByIdUseCase?: GetCafeteriaByIdUseCase;
private _getCafeteriaMenuByDateUseCase?: GetCafeteriaMenuByDateUseCase;
private _getCafeteriaMenuTimelineUseCase?: GetCafeteriaMenuTimelineUseCase;
private _getCafeteriaMenuAvailabilityUseCase?: GetCafeteriaMenuAvailabilityUseCase;
private _registerCafeteriaUseCase?: RegisterCafeteriaUseCase;
private _registerCafeteriaMenuUseCase?: RegisterCafeteriaMenuUseCase;

// Review UseCases (Lazy)
private _createReviewUseCase?: CreateReviewUseCase;
private _getReviewCommentsUseCase?: GetReviewCommentsUseCase;
private _createReviewCommentUseCase?: CreateReviewCommentUseCase;
private _createReviewCommentReplyUseCase?: CreateReviewCommentReplyUseCase;

// Data Layer Getters (Private - Lazy Initialization)
private getCafeteriaDataSource(): CafeteriaRemoteDataSourceMockImpl {
private getSessionManager(): ClientSessionManager {
if (!this._sessionManager) {
this._sessionManager = new ClientSessionManager();
}
return this._sessionManager;
}

private getTokenProvider(): ClientTokenProvider {
if (!this._tokenProvider) {
this._tokenProvider = new ClientTokenProvider(this.getSessionManager());
}
return this._tokenProvider;
}

private getBaseClient(): FetchHttpClient {
if (!this._baseClient) {
this._baseClient = new FetchHttpClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
});
}
return this._baseClient;
}

private getHttpClient(): HttpClient {
if (!this._httpClient) {
this._httpClient = new AuthenticatedHttpClient(
this.getBaseClient(),
this.getTokenProvider(),
this.getSessionManager(),
undefined,
);
}
return this._httpClient;
}

private getCafeteriaDataSource(): CafeteriaRemoteDataSourceImpl {
if (!this._cafeteriaDataSource) {
this._cafeteriaDataSource = new CafeteriaRemoteDataSourceMockImpl();
this._cafeteriaDataSource = new CafeteriaRemoteDataSourceImpl(
this.getHttpClient(),
);
}
return this._cafeteriaDataSource;
}
Expand Down Expand Up @@ -106,103 +137,53 @@ class CafeteriaClientContainerImpl implements CafeteriaClientContainer {
return this._reviewRepository;
}

// Cafeteria UseCase Getters (Public - Lazy Initialization)
getGetCafeteriasWithMenu(): GetCafeteriasWithMenuUseCase {
if (!this._getCafeteriasWithMenuUseCase) {
this._getCafeteriasWithMenuUseCase = new GetCafeteriasWithMenuUseCaseImpl(
this.getCafeteriaRepository(),
);
}
return this._getCafeteriasWithMenuUseCase;
return new GetCafeteriasWithMenuUseCaseImpl(this.getCafeteriaRepository());
}

getGetCafeteriaById(): GetCafeteriaByIdUseCase {
if (!this._getCafeteriaByIdUseCase) {
this._getCafeteriaByIdUseCase = new GetCafeteriaByIdUseCaseImpl(
this.getCafeteriaRepository(),
);
}
return this._getCafeteriaByIdUseCase;
return new GetCafeteriaByIdUseCaseImpl(this.getCafeteriaRepository());
}

getGetCafeteriaMenuByDate(): GetCafeteriaMenuByDateUseCase {
if (!this._getCafeteriaMenuByDateUseCase) {
this._getCafeteriaMenuByDateUseCase =
new GetCafeteriaMenuByDateUseCaseImpl(this.getCafeteriaRepository());
}
return this._getCafeteriaMenuByDateUseCase;
return new GetCafeteriaMenuByDateUseCaseImpl(this.getCafeteriaRepository());
}

getGetCafeteriaMenuTimeline(): GetCafeteriaMenuTimelineUseCase {
if (!this._getCafeteriaMenuTimelineUseCase) {
this._getCafeteriaMenuTimelineUseCase =
new GetCafeteriaMenuTimelineUseCaseImpl(this.getCafeteriaRepository());
}
return this._getCafeteriaMenuTimelineUseCase;
return new GetCafeteriaMenuTimelineUseCaseImpl(
this.getCafeteriaRepository(),
);
}

getGetCafeteriaMenuAvailability(): GetCafeteriaMenuAvailabilityUseCase {
if (!this._getCafeteriaMenuAvailabilityUseCase) {
this._getCafeteriaMenuAvailabilityUseCase =
new GetCafeteriaMenuAvailabilityUseCaseImpl(
this.getCafeteriaRepository(),
);
}
return this._getCafeteriaMenuAvailabilityUseCase;
return new GetCafeteriaMenuAvailabilityUseCaseImpl(
this.getCafeteriaRepository(),
);
}

getRegisterCafeteria(): RegisterCafeteriaUseCase {
if (!this._registerCafeteriaUseCase) {
this._registerCafeteriaUseCase = new RegisterCafeteriaUseCaseImpl(
this.getCafeteriaRepository(),
);
}
return this._registerCafeteriaUseCase;
return new RegisterCafeteriaUseCaseImpl(this.getCafeteriaRepository());
}

getRegisterCafeteriaMenu(): RegisterCafeteriaMenuUseCase {
if (!this._registerCafeteriaMenuUseCase) {
this._registerCafeteriaMenuUseCase = new RegisterCafeteriaMenuUseCaseImpl(
this.getCafeteriaRepository(),
);
}
return this._registerCafeteriaMenuUseCase;
return new RegisterCafeteriaMenuUseCaseImpl(this.getCafeteriaRepository());
}

// Review UseCase Getters (Public - Lazy Initialization)
// Review UseCase Getters (Public - 매번 새로 생성)
getCreateReview(): CreateReviewUseCase {
if (!this._createReviewUseCase) {
this._createReviewUseCase = new CreateReviewUseCaseImpl(
this.getReviewRepository(),
);
}
return this._createReviewUseCase;
return new CreateReviewUseCaseImpl(this.getReviewRepository());
}

getGetReviewComments(): GetReviewCommentsUseCase {
if (!this._getReviewCommentsUseCase) {
this._getReviewCommentsUseCase = new GetReviewCommentsUseCaseImpl(
this.getReviewRepository(),
);
}
return this._getReviewCommentsUseCase;
return new GetReviewCommentsUseCaseImpl(this.getReviewRepository());
}

getCreateReviewComment(): CreateReviewCommentUseCase {
if (!this._createReviewCommentUseCase) {
this._createReviewCommentUseCase = new CreateReviewCommentUseCaseImpl(
this.getReviewRepository(),
);
}
return this._createReviewCommentUseCase;
return new CreateReviewCommentUseCaseImpl(this.getReviewRepository());
}

getCreateReviewCommentReply(): CreateReviewCommentReplyUseCase {
if (!this._createReviewCommentReplyUseCase) {
this._createReviewCommentReplyUseCase =
new CreateReviewCommentReplyUseCaseImpl(this.getReviewRepository());
}
return this._createReviewCommentReplyUseCase;
return new CreateReviewCommentReplyUseCaseImpl(this.getReviewRepository());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "server-only";

// Data Layer
import {
CafeteriaRemoteDataSourceMockImpl,
CafeteriaRemoteDataSourceImpl,
CafeteriaRepositoryImpl,
CafeteriaReviewRemoteDataSourceMockImpl,
CafeteriaReviewRepositoryImpl,
Expand Down Expand Up @@ -77,8 +77,8 @@ class CafeteriaServerContainer {
refreshTokenService, // Server-side: RefreshTokenService 주입
);

// Data Layer - Use mock DataSource for both cafeteria and reviews (for development)
const cafeteriaDataSource = new CafeteriaRemoteDataSourceMockImpl();
// Data Layer
const cafeteriaDataSource = new CafeteriaRemoteDataSourceImpl(_httpClient);
const cafeteriaRepository = new CafeteriaRepositoryImpl(
cafeteriaDataSource,
);
Expand Down
Loading