From c5fab7d61a2da3b2a3d6b9024a59440b2c6ca6ba Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Tue, 14 Oct 2025 11:53:57 +0800 Subject: [PATCH 01/10] refactor: dependency injection to use a centralized Container class for managing application state and services --- frontend/src/app.d.ts | 4 +- frontend/src/hooks.server.ts | 29 +--- .../lib/auth/adapter/presenter/authBloc.ts | 10 +- frontend/src/lib/container.ts | 134 ++++++++++++++++++ .../src/lib/home/framework/ui/Motto.svelte | 2 +- .../framework/ui/ImageManagementPage.svelte | 2 +- .../framework/ui/UploadImageDialoag.svelte | 4 +- frontend/src/routes/+layout.svelte | 5 + frontend/src/routes/dashboard/+layout.svelte | 17 +-- frontend/src/routes/post/+page.server.ts | 3 +- frontend/src/routes/post/+page.svelte | 18 +-- frontend/src/routes/post/[id]/+page.server.ts | 3 +- frontend/src/routes/post/[id]/+page.svelte | 16 +-- 13 files changed, 166 insertions(+), 81 deletions(-) create mode 100644 frontend/src/lib/container.ts diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 3d0dcb6..eb3f9c6 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -5,9 +5,7 @@ declare global { // interface Error {} interface Locals { - authBloc: import('$lib/auth/adapter/presenter/authBloc').AuthBloc; - postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc; - postBloc: import('$lib/post/adapter/presenter/postBloc').PostBloc; + container: import('$lib/container').Container; } // interface PageData {} diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index e6675b7..e1e1b4e 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,21 +1,8 @@ import { sequence } from '@sveltejs/kit/hooks'; import * as Sentry from '@sentry/sveltekit'; -import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl'; -import { PostBloc } from '$lib/post/adapter/presenter/postBloc'; -import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc'; -import { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; -import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; -import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; import type { Handle } from '@sveltejs/kit'; import { Environment } from '$lib/environment'; -import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl'; -import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl'; -import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; -import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; -import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; -import type { PostRepository } from '$lib/post/application/gateway/postRepository'; -import { AuthBloc } from '$lib/auth/adapter/presenter/authBloc'; -import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; +import { Container } from '$lib/container'; Sentry.init({ dsn: Environment.SENTRY_DSN, @@ -24,18 +11,8 @@ Sentry.init({ }); export const handle: Handle = sequence(Sentry.sentryHandle(), ({ event, resolve }) => { - const authApiService: AuthApiService = new AuthApiServiceImpl(event.fetch); - const authRepository: AuthRepository = new AuthRepositoryImpl(authApiService); - const getCurrentUserUseCase = new GetCurrentUserUseCase(authRepository); - - const postApiService: PostApiService = new PostApiServiceImpl(event.fetch); - const postRepository: PostRepository = new PostRepositoryImpl(postApiService); - const getAllPostsUseCase = new GetAllPostsUseCase(postRepository); - const getPostUseCase = new GetPostUseCase(postRepository); - - event.locals.authBloc = new AuthBloc(getCurrentUserUseCase); - event.locals.postListBloc = new PostListBloc(getAllPostsUseCase); - event.locals.postBloc = new PostBloc(getPostUseCase); + const container = new Container(event.fetch); + event.locals.container = container; return resolve(event); }); diff --git a/frontend/src/lib/auth/adapter/presenter/authBloc.ts b/frontend/src/lib/auth/adapter/presenter/authBloc.ts index a793cf3..2a962a4 100644 --- a/frontend/src/lib/auth/adapter/presenter/authBloc.ts +++ b/frontend/src/lib/auth/adapter/presenter/authBloc.ts @@ -12,15 +12,7 @@ export class AuthBloc { status: StatusType.Idle, }); - constructor( - private readonly getCurrentUserUseCase: GetCurrentUserUseCase, - initialData?: AuthViewModel - ) { - this.state.set({ - status: StatusType.Idle, - data: initialData, - }); - } + constructor(private readonly getCurrentUserUseCase: GetCurrentUserUseCase) {} get subscribe() { return this.state.subscribe; diff --git a/frontend/src/lib/container.ts b/frontend/src/lib/container.ts new file mode 100644 index 0000000..10a7785 --- /dev/null +++ b/frontend/src/lib/container.ts @@ -0,0 +1,134 @@ +import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; +import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl'; +import { AuthBloc } from '$lib/auth/adapter/presenter/authBloc'; +import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; +import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; +import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl'; +import type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService'; +import { ImageRepositoryImpl } from '$lib/image/adapter/gateway/imageRepositoryImpl'; +import { ImageBloc } from '$lib/image/adapter/presenter/imageBloc'; +import type { ImageRepository } from '$lib/image/application/gateway/imageRepository'; +import { UploadImageUseCase } from '$lib/image/application/useCase/uploadImageUseCase'; +import { ImageApiServiceImpl } from '$lib/image/framework/api/imageApiServiceImpl'; +import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; +import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl'; +import { PostBloc } from '$lib/post/adapter/presenter/postBloc'; +import type { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; +import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc'; +import type { PostViewModel } from '$lib/post/adapter/presenter/postViewModel'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; +import { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; +import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; +import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; + +export class Container { + private useCases: UseCases; + + constructor(fetchFn: typeof fetch) { + const apiServices = new ApiServices(fetchFn); + const repositories = new Repositories(apiServices); + this.useCases = new UseCases(repositories); + } + + createAuthBloc(): AuthBloc { + return new AuthBloc(this.useCases.getCurrentUserUseCase); + } + + createImageBloc(): ImageBloc { + return new ImageBloc(this.useCases.uploadImageUseCase); + } + + createPostListBloc(initialData?: readonly PostInfoViewModel[]): PostListBloc { + return new PostListBloc(this.useCases.getAllPostsUseCase, initialData); + } + + createPostBloc(initialData?: PostViewModel): PostBloc { + return new PostBloc(this.useCases.getPostUseCase, initialData); + } +} + +class ApiServices { + private fetchFn: typeof fetch; + + private _authApiService?: AuthApiService; + private _imageApiService?: ImageApiService; + private _postApiService?: PostApiService; + + constructor(fetchFn: typeof fetch) { + this.fetchFn = fetchFn; + } + + get authApiService(): AuthApiService { + this._authApiService ??= new AuthApiServiceImpl(this.fetchFn); + return this._authApiService; + } + + get imageApiService(): ImageApiService { + this._imageApiService ??= new ImageApiServiceImpl(this.fetchFn); + return this._imageApiService; + } + + get postApiService(): PostApiService { + this._postApiService ??= new PostApiServiceImpl(this.fetchFn); + return this._postApiService; + } +} + +class Repositories { + private apiServices: ApiServices; + + private _authRepository?: AuthRepository; + private _imageRepository?: ImageRepository; + private _postRepository?: PostRepository; + + constructor(apiServices: ApiServices) { + this.apiServices = apiServices; + } + + get authRepository(): AuthRepository { + this._authRepository ??= new AuthRepositoryImpl(this.apiServices.authApiService); + return this._authRepository; + } + + get imageRepository(): ImageRepository { + this._imageRepository ??= new ImageRepositoryImpl(this.apiServices.imageApiService); + return this._imageRepository; + } + + get postRepository(): PostRepository { + this._postRepository ??= new PostRepositoryImpl(this.apiServices.postApiService); + return this._postRepository; + } +} + +class UseCases { + private repositories: Repositories; + + private _getCurrentUserUseCase?: GetCurrentUserUseCase; + private _uploadImageUseCase?: UploadImageUseCase; + private _getAllPostsUseCase?: GetAllPostsUseCase; + private _getPostUseCase?: GetPostUseCase; + + constructor(repositories: Repositories) { + this.repositories = repositories; + } + + get getCurrentUserUseCase(): GetCurrentUserUseCase { + this._getCurrentUserUseCase ??= new GetCurrentUserUseCase(this.repositories.authRepository); + return this._getCurrentUserUseCase; + } + + get uploadImageUseCase(): UploadImageUseCase { + this._uploadImageUseCase ??= new UploadImageUseCase(this.repositories.imageRepository); + return this._uploadImageUseCase; + } + + get getAllPostsUseCase(): GetAllPostsUseCase { + this._getAllPostsUseCase ??= new GetAllPostsUseCase(this.repositories.postRepository); + return this._getAllPostsUseCase; + } + get getPostUseCase(): GetPostUseCase { + this._getPostUseCase ??= new GetPostUseCase(this.repositories.postRepository); + return this._getPostUseCase; + } +} diff --git a/frontend/src/lib/home/framework/ui/Motto.svelte b/frontend/src/lib/home/framework/ui/Motto.svelte index feb1f01..efcf59f 100644 --- a/frontend/src/lib/home/framework/ui/Motto.svelte +++ b/frontend/src/lib/home/framework/ui/Motto.svelte @@ -1,5 +1,5 @@
import { getContext } from 'svelte'; - import UploadImageDialoag from './UploadImageDialoag.svelte'; + import UploadImageDialoag from '$lib/image/framework/ui/UploadImageDialoag.svelte'; import { ImageBloc, ImageEventType } from '$lib/image/adapter/presenter/imageBloc'; import { StatusType } from '$lib/common/adapter/presenter/asyncState'; import { toast } from 'svelte-sonner'; diff --git a/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte b/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte index a54860e..792e5cd 100644 --- a/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte +++ b/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte @@ -57,7 +57,7 @@ Upload Image -
+ @@ -77,7 +77,7 @@ - + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 7a87f94..e8ee721 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -5,6 +5,11 @@ import '../app.css'; import '@fortawesome/fontawesome-free/css/all.min.css'; import { Toaster } from '$lib/common/framework/components/ui/sonner'; + import { Container } from '$lib/container'; + import { setContext } from 'svelte'; + + const container = new Container(fetch); + setContext(Container.name, container); diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 47f29e8..0f6b7d7 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -1,24 +1,17 @@ diff --git a/frontend/src/routes/post/[id]/+page.server.ts b/frontend/src/routes/post/[id]/+page.server.ts index a065024..abab035 100644 --- a/frontend/src/routes/post/[id]/+page.server.ts +++ b/frontend/src/routes/post/[id]/+page.server.ts @@ -3,7 +3,8 @@ import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals, params }) => { - const { postBloc } = locals; + const { container } = locals; + const postBloc = container.createPostBloc(); const state = await postBloc.dispatch({ event: PostEventType.PostLoadedEvent, diff --git a/frontend/src/routes/post/[id]/+page.svelte b/frontend/src/routes/post/[id]/+page.svelte index 5e14b25..34d156f 100644 --- a/frontend/src/routes/post/[id]/+page.svelte +++ b/frontend/src/routes/post/[id]/+page.svelte @@ -1,25 +1,17 @@ -- 2.47.2 From 13696e394f47679d78692f64f54be802831f6636 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Tue, 14 Oct 2025 15:34:34 +0800 Subject: [PATCH 02/10] refactor: enhance error handling and state management in auth and image modules --- .../lib/auth/adapter/presenter/authBloc.ts | 27 +++++++----- .../auth/framework/api/authApiServiceImpl.ts | 8 +++- .../common/adapter/presenter/asyncState.ts | 41 ++++++++++++++++--- .../src/lib/common/framework/web/httpError.ts | 8 ++++ .../common/framework/web/httpStatusCode.ts | 4 ++ .../lib/image/adapter/presenter/imageBloc.ts | 16 +++++--- .../framework/api/imageApiServiceImpl.ts | 4 +- .../lib/post/adapter/presenter/postBloc.ts | 32 +++++++++------ .../post/adapter/presenter/postListBloc.ts | 25 +++++++---- .../post/framework/api/postApiServiceImpl.ts | 10 ++++- frontend/src/routes/dashboard/+layout.svelte | 14 ++----- 11 files changed, 135 insertions(+), 54 deletions(-) create mode 100644 frontend/src/lib/common/framework/web/httpError.ts create mode 100644 frontend/src/lib/common/framework/web/httpStatusCode.ts diff --git a/frontend/src/lib/auth/adapter/presenter/authBloc.ts b/frontend/src/lib/auth/adapter/presenter/authBloc.ts index 2a962a4..1d1c7d8 100644 --- a/frontend/src/lib/auth/adapter/presenter/authBloc.ts +++ b/frontend/src/lib/auth/adapter/presenter/authBloc.ts @@ -1,7 +1,12 @@ import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel'; import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel'; import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type AuthState = AsyncState; @@ -26,16 +31,18 @@ export class AuthBloc { } private async loadCurrentUser(): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + this.state.set(StateFactory.loading(get(this.state).data)); - const user = await this.getCurrentUserUseCase.execute(); - - const userViewModel = user ? UserViewModel.fromEntity(user) : null; - const authViewModel = AuthViewModel.fromEntity(userViewModel); - const result: AuthState = { - status: StatusType.Success, - data: authViewModel, - }; + let result: AuthState; + try { + const user = await this.getCurrentUserUseCase.execute(); + const userViewModel = user ? UserViewModel.fromEntity(user) : null; + const authViewModel = AuthViewModel.fromEntity(userViewModel); + result = StateFactory.success(authViewModel); + } catch (e) { + result = StateFactory.error(e); + captureException(e); + } this.state.set(result); return result; diff --git a/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts index f041c0e..31b72e0 100644 --- a/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts +++ b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts @@ -1,5 +1,7 @@ import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; import { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto'; +import { HttpError } from '$lib/common/framework/web/httpError'; +import { HttpStatusCode } from '$lib/common/framework/web/httpStatusCode'; import { Environment } from '$lib/environment'; export class AuthApiServiceImpl implements AuthApiService { @@ -10,10 +12,14 @@ export class AuthApiServiceImpl implements AuthApiService { const response = await this.fetchFn(url); - if (!response.ok) { + if (response.status === HttpStatusCode.UNAUTHORIZED) { return null; } + if (!response.ok) { + throw new HttpError(response.status, url); + } + const json = await response.json(); return UserResponseDto.fromJson(json); } diff --git a/frontend/src/lib/common/adapter/presenter/asyncState.ts b/frontend/src/lib/common/adapter/presenter/asyncState.ts index 19e10cf..bd90704 100644 --- a/frontend/src/lib/common/adapter/presenter/asyncState.ts +++ b/frontend/src/lib/common/adapter/presenter/asyncState.ts @@ -5,25 +5,56 @@ export enum StatusType { Error, } -export interface IdleState { +export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; + +interface IdleState { status: StatusType.Idle; data?: T; } -export interface LoadingState { +interface LoadingState { status: StatusType.Loading; data?: T; } -export interface SuccessState { +interface SuccessState { status: StatusType.Success; data: T; } -export interface ErrorState { +interface ErrorState { status: StatusType.Error; data?: T; error: Error; } -export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; +export abstract class StateFactory { + static idle(data?: T): AsyncState { + return { + status: StatusType.Idle, + data, + }; + } + + static loading(data?: T): AsyncState { + return { + status: StatusType.Loading, + data, + }; + } + + static success(data: T): AsyncState { + return { + status: StatusType.Success, + data, + }; + } + + static error(error: unknown, data?: T): AsyncState { + return { + status: StatusType.Error, + error: error instanceof Error ? error : new Error('Unknown error'), + data, + }; + } +} diff --git a/frontend/src/lib/common/framework/web/httpError.ts b/frontend/src/lib/common/framework/web/httpError.ts new file mode 100644 index 0000000..207d828 --- /dev/null +++ b/frontend/src/lib/common/framework/web/httpError.ts @@ -0,0 +1,8 @@ +export class HttpError extends Error { + constructor( + public readonly status: number, + url: URL + ) { + super(`HTTP ${status} at ${url.href}`); + } +} diff --git a/frontend/src/lib/common/framework/web/httpStatusCode.ts b/frontend/src/lib/common/framework/web/httpStatusCode.ts new file mode 100644 index 0000000..071763b --- /dev/null +++ b/frontend/src/lib/common/framework/web/httpStatusCode.ts @@ -0,0 +1,4 @@ +export enum HttpStatusCode { + UNAUTHORIZED = 401, + NOT_FOUND = 404, +} diff --git a/frontend/src/lib/image/adapter/presenter/imageBloc.ts b/frontend/src/lib/image/adapter/presenter/imageBloc.ts index 7fceeb1..ee44433 100644 --- a/frontend/src/lib/image/adapter/presenter/imageBloc.ts +++ b/frontend/src/lib/image/adapter/presenter/imageBloc.ts @@ -1,6 +1,11 @@ -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; import { ImageInfoViewModel } from '$lib/image/adapter/presenter/imageInfoViewModel'; import type { UploadImageUseCase } from '$lib/image/application/useCase/uploadImageUseCase'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type ImageInfoState = AsyncState; @@ -25,15 +30,16 @@ export class ImageBloc { } private async uploadImage(file: File): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + this.state.set(StateFactory.loading(get(this.state).data)); let result: ImageInfoState; try { const imageInfo = await this.uploadImageUseCase.execute(file); const imageInfoViewModel = ImageInfoViewModel.fromEntity(imageInfo); - result = { status: StatusType.Success, data: imageInfoViewModel }; - } catch (error) { - result = { status: StatusType.Error, error: error as Error }; + result = StateFactory.success(imageInfoViewModel); + } catch (e) { + result = StateFactory.error(e); + captureException(e); } return result; diff --git a/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts b/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts index 276aaf4..813fc0c 100644 --- a/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts +++ b/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts @@ -1,3 +1,4 @@ +import { HttpError } from '$lib/common/framework/web/httpError'; import { Environment } from '$lib/environment'; import type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService'; import { ImageInfoResponseDto } from '$lib/image/adapter/gateway/imageInfoResponseDto'; @@ -17,8 +18,9 @@ export class ImageApiServiceImpl implements ImageApiService { }); if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`); + throw new HttpError(response.status, url); } + const data = await response.json(); return ImageInfoResponseDto.fromJson(data); } diff --git a/frontend/src/lib/post/adapter/presenter/postBloc.ts b/frontend/src/lib/post/adapter/presenter/postBloc.ts index a0fb224..0b6190e 100644 --- a/frontend/src/lib/post/adapter/presenter/postBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postBloc.ts @@ -1,6 +1,11 @@ -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel'; import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type PostState = AsyncState; @@ -33,20 +38,23 @@ export class PostBloc { } private async loadPost(id: string): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + this.state.set(StateFactory.loading(get(this.state).data)); - const post = await this.getPostUseCase.execute(id); - if (!post) { - this.state.set({ status: StatusType.Error, error: new Error('Post not found') }); - return get(this.state); + let result: PostState; + try { + const post = await this.getPostUseCase.execute(id); + if (!post) { + result = StateFactory.error(new Error('Post not found')); + this.state.set(result); + return result; + } + const postViewModel = PostViewModel.fromEntity(post); + result = StateFactory.success(postViewModel); + } catch (e) { + result = StateFactory.error(e); + captureException(e); } - const postViewModel = PostViewModel.fromEntity(post); - const result: PostState = { - status: StatusType.Success, - data: postViewModel, - }; - this.state.set(result); return result; } diff --git a/frontend/src/lib/post/adapter/presenter/postListBloc.ts b/frontend/src/lib/post/adapter/presenter/postListBloc.ts index 829c38d..48fbcf1 100644 --- a/frontend/src/lib/post/adapter/presenter/postListBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postListBloc.ts @@ -1,6 +1,11 @@ -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; import type { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type PostListState = AsyncState; @@ -33,13 +38,17 @@ export class PostListBloc { } private async loadPosts(): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); - const posts = await this.getAllPostsUseCase.execute(); - const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post)); - const result: PostListState = { - status: StatusType.Success, - data: postViewModels, - }; + this.state.set(StateFactory.loading(get(this.state).data)); + + let result: PostListState; + try { + const posts = await this.getAllPostsUseCase.execute(); + const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post)); + result = StateFactory.success(postViewModels); + } catch (e) { + result = StateFactory.error(e); + captureException(e); + } this.state.set(result); return result; diff --git a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts index d00a847..18d5485 100644 --- a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts +++ b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts @@ -1,3 +1,5 @@ +import { HttpError } from '$lib/common/framework/web/httpError'; +import { HttpStatusCode } from '$lib/common/framework/web/httpStatusCode'; import { Environment } from '$lib/environment'; import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto'; @@ -12,7 +14,7 @@ export class PostApiServiceImpl implements PostApiService { const response = await this.fetchFn(url); if (!response.ok) { - return []; + throw new HttpError(response.status, url); } const json = await response.json(); @@ -24,10 +26,14 @@ export class PostApiServiceImpl implements PostApiService { const response = await this.fetchFn(url); - if (!response.ok) { + if (response.status === HttpStatusCode.NOT_FOUND) { return null; } + if (!response.ok) { + throw new HttpError(response.status, url); + } + const json = await response.json(); return PostResponseDto.fromJson(json); } diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 0f6b7d7..73bedb6 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -20,15 +20,9 @@ const isLoading = $derived( authState.status === StatusType.Loading || authState.status === StatusType.Idle ); - const hasError = $derived.by(() => { - if (authState.status === StatusType.Error) { - return true; - } - if (authState.status === StatusType.Success && !authState.data.isAuthenticated) { - return true; - } - return false; - }); + const isAuthenticated = $derived( + authState.status === StatusType.Success && authState.data.isAuthenticated + ); const links: DashboardLink[] = [ { label: 'Post', href: '/dashboard/post' }, @@ -39,7 +33,7 @@ {#if isLoading}
-{:else if hasError} +{:else if !isAuthenticated} {:else}
-- 2.47.2 From d569f35bc1895eedd207aff6d7d7e4e8507ed726 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Tue, 14 Oct 2025 20:50:06 +0800 Subject: [PATCH 03/10] feat: implement create post functionality with dialog and API integration --- frontend/src/lib/container.ts | 10 +- .../framework/ui/UploadImageDialoag.svelte | 8 +- .../adapter/gateway/creatPostRequestDto.ts | 52 +++++++++ .../post/adapter/gateway/postApiService.ts | 2 + .../adapter/gateway/postRepositoryImpl.ts | 9 +- .../lib/post/adapter/presenter/postBloc.ts | 32 +++++- .../application/gateway/postRepository.ts | 6 + .../application/useCase/createPostUseCase.ts | 13 +++ .../post/framework/api/postApiServiceImpl.ts | 26 ++++- .../post/framework/ui/CreatePostDialog.svelte | 107 ++++++++++++++++++ .../framework/ui/PostManagementPage.svelte | 35 ++++++ frontend/src/routes/+error.svelte | 2 +- frontend/src/routes/+page.svelte | 2 +- .../src/routes/dashboard/image/+page.svelte | 15 +-- .../src/routes/dashboard/post/+page.svelte | 13 ++- 15 files changed, 305 insertions(+), 27 deletions(-) create mode 100644 frontend/src/lib/post/adapter/gateway/creatPostRequestDto.ts create mode 100644 frontend/src/lib/post/application/useCase/createPostUseCase.ts create mode 100644 frontend/src/lib/post/framework/ui/CreatePostDialog.svelte create mode 100644 frontend/src/lib/post/framework/ui/PostManagementPage.svelte diff --git a/frontend/src/lib/container.ts b/frontend/src/lib/container.ts index 10a7785..42bcd94 100644 --- a/frontend/src/lib/container.ts +++ b/frontend/src/lib/container.ts @@ -17,6 +17,7 @@ import type { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoView import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc'; import type { PostViewModel } from '$lib/post/adapter/presenter/postViewModel'; import type { PostRepository } from '$lib/post/application/gateway/postRepository'; +import { CreatePostUseCase } from '$lib/post/application/useCase/createPostUseCase'; import { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; @@ -43,7 +44,7 @@ export class Container { } createPostBloc(initialData?: PostViewModel): PostBloc { - return new PostBloc(this.useCases.getPostUseCase, initialData); + return new PostBloc(this.useCases.getPostUseCase, this.useCases.createPostUseCase, initialData); } } @@ -108,6 +109,7 @@ class UseCases { private _uploadImageUseCase?: UploadImageUseCase; private _getAllPostsUseCase?: GetAllPostsUseCase; private _getPostUseCase?: GetPostUseCase; + private _createPostUseCase?: CreatePostUseCase; constructor(repositories: Repositories) { this.repositories = repositories; @@ -127,8 +129,14 @@ class UseCases { this._getAllPostsUseCase ??= new GetAllPostsUseCase(this.repositories.postRepository); return this._getAllPostsUseCase; } + get getPostUseCase(): GetPostUseCase { this._getPostUseCase ??= new GetPostUseCase(this.repositories.postRepository); return this._getPostUseCase; } + + get createPostUseCase(): CreatePostUseCase { + this._createPostUseCase ??= new CreatePostUseCase(this.repositories.postRepository); + return this._createPostUseCase; + } } diff --git a/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte b/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte index 792e5cd..4f89d61 100644 --- a/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte +++ b/frontend/src/lib/image/framework/ui/UploadImageDialoag.svelte @@ -40,13 +40,9 @@ files = undefined; fileInputErrorMessage = null; } - - function close() { - open = false; - } - (open = val)}> + Upload - + diff --git a/frontend/src/lib/post/adapter/gateway/creatPostRequestDto.ts b/frontend/src/lib/post/adapter/gateway/creatPostRequestDto.ts new file mode 100644 index 0000000..0f67bc6 --- /dev/null +++ b/frontend/src/lib/post/adapter/gateway/creatPostRequestDto.ts @@ -0,0 +1,52 @@ +import type { CreatePostParams } from '$lib/post/application/gateway/postRepository'; + +export class CreatePostRequestDto { + readonly semanticId: string; + readonly title: string; + readonly description: string; + readonly content: string; + readonly labelIds: number[]; + readonly previewImageUrl: URL; + readonly publishedTime: Date | null; + + private constructor(props: { + semanticId: string; + title: string; + description: string; + content: string; + labelIds: number[]; + previewImageUrl: URL; + publishedTime: Date | null; + }) { + this.semanticId = props.semanticId; + this.title = props.title; + this.description = props.description; + this.content = props.content; + this.labelIds = props.labelIds; + this.previewImageUrl = props.previewImageUrl; + this.publishedTime = props.publishedTime; + } + + static fromParams(params: CreatePostParams): CreatePostRequestDto { + return new CreatePostRequestDto({ + ...params, + description: '', + content: '', + labelIds: [], + previewImageUrl: new URL('https://example.com'), + publishedTime: null, + }); + } + + toJson() { + return { + semantic_id: this.semanticId, + title: this.title, + description: this.description, + content: this.content, + label_ids: this.labelIds, + preview_image_url: this.previewImageUrl, + published_time: this.publishedTime, + }; + } +} diff --git a/frontend/src/lib/post/adapter/gateway/postApiService.ts b/frontend/src/lib/post/adapter/gateway/postApiService.ts index a8503d7..d9f4a31 100644 --- a/frontend/src/lib/post/adapter/gateway/postApiService.ts +++ b/frontend/src/lib/post/adapter/gateway/postApiService.ts @@ -1,7 +1,9 @@ +import type { CreatePostRequestDto } from '$lib/post/adapter/gateway/creatPostRequestDto'; import type { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto'; import type { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto'; export interface PostApiService { getAllPosts(): Promise; getPost(id: string): Promise; + createPost(payload: CreatePostRequestDto): Promise; } diff --git a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts index e75dfed..764b3cc 100644 --- a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts +++ b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts @@ -1,5 +1,6 @@ +import { CreatePostRequestDto } from '$lib/post/adapter/gateway/creatPostRequestDto'; import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; -import type { PostRepository } from '$lib/post/application/gateway/postRepository'; +import type { CreatePostParams, PostRepository } from '$lib/post/application/gateway/postRepository'; import type { Post } from '$lib/post/domain/entity/post'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; @@ -15,4 +16,10 @@ export class PostRepositoryImpl implements PostRepository { const dto = await this.postApiService.getPost(id); return dto?.toEntity() ?? null; } + + async createPost(params: CreatePostParams): Promise { + const requestDto = CreatePostRequestDto.fromParams(params); + const responseDto = await this.postApiService.createPost(requestDto); + return responseDto.toEntity(); + } } diff --git a/frontend/src/lib/post/adapter/presenter/postBloc.ts b/frontend/src/lib/post/adapter/presenter/postBloc.ts index 0b6190e..35a6f92 100644 --- a/frontend/src/lib/post/adapter/presenter/postBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postBloc.ts @@ -4,12 +4,14 @@ import { type AsyncState, } from '$lib/common/adapter/presenter/asyncState'; import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel'; +import type { CreatePostParams } from '$lib/post/application/gateway/postRepository'; +import type { CreatePostUseCase } from '$lib/post/application/useCase/createPostUseCase'; import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type PostState = AsyncState; -export type PostEvent = PostLoadedEvent; +export type PostEvent = PostLoadedEvent | PostCreatedEvent; export class PostBloc { private readonly state = writable({ @@ -18,6 +20,7 @@ export class PostBloc { constructor( private readonly getPostUseCase: GetPostUseCase, + private readonly createPostUseCase: CreatePostUseCase, initialData?: PostViewModel ) { this.state.set({ @@ -34,6 +37,9 @@ export class PostBloc { switch (event.event) { case PostEventType.PostLoadedEvent: return this.loadPost(event.id); + case PostEventType.PostCreatedEvent: + const { semanticId, title } = event; + return this.createPost({ semanticId, title }); } } @@ -58,13 +64,37 @@ export class PostBloc { this.state.set(result); return result; } + + private async createPost(params: CreatePostParams): Promise { + this.state.set(StateFactory.loading(get(this.state).data)); + + let result: PostState; + try { + const post = await this.createPostUseCase.execute(params); + const postViewModel = PostViewModel.fromEntity(post); + result = StateFactory.success(postViewModel); + } catch (e) { + result = StateFactory.error(e); + captureException(e); + } + + this.state.set(result); + return result; + } } export enum PostEventType { PostLoadedEvent, + PostCreatedEvent, } interface PostLoadedEvent { event: PostEventType.PostLoadedEvent; id: string; } + +interface PostCreatedEvent { + event: PostEventType.PostCreatedEvent; + semanticId: string; + title: string; +} diff --git a/frontend/src/lib/post/application/gateway/postRepository.ts b/frontend/src/lib/post/application/gateway/postRepository.ts index 664a263..bff44f9 100644 --- a/frontend/src/lib/post/application/gateway/postRepository.ts +++ b/frontend/src/lib/post/application/gateway/postRepository.ts @@ -4,4 +4,10 @@ import type { PostInfo } from '$lib/post/domain/entity/postInfo'; export interface PostRepository { getAllPosts(): Promise; getPost(id: string): Promise; + createPost(params: CreatePostParams): Promise; +} + +export interface CreatePostParams { + semanticId: string; + title: string; } diff --git a/frontend/src/lib/post/application/useCase/createPostUseCase.ts b/frontend/src/lib/post/application/useCase/createPostUseCase.ts new file mode 100644 index 0000000..cec2d2e --- /dev/null +++ b/frontend/src/lib/post/application/useCase/createPostUseCase.ts @@ -0,0 +1,13 @@ +import type { + CreatePostParams, + PostRepository, +} from '$lib/post/application/gateway/postRepository'; +import type { Post } from '$lib/post/domain/entity/post'; + +export class CreatePostUseCase { + constructor(private readonly postRepository: PostRepository) {} + + async execute(params: CreatePostParams): Promise { + return this.postRepository.createPost(params); + } +} diff --git a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts index 18d5485..13359b6 100644 --- a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts +++ b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts @@ -1,6 +1,7 @@ import { HttpError } from '$lib/common/framework/web/httpError'; import { HttpStatusCode } from '$lib/common/framework/web/httpStatusCode'; import { Environment } from '$lib/environment'; +import type { CreatePostRequestDto } from '$lib/post/adapter/gateway/creatPostRequestDto'; import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto'; import { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto'; @@ -17,8 +18,8 @@ export class PostApiServiceImpl implements PostApiService { throw new HttpError(response.status, url); } - const json = await response.json(); - return json.map(PostInfoResponseDto.fromJson); + const data = await response.json(); + return data.map(PostInfoResponseDto.fromJson); } async getPost(id: string): Promise { @@ -34,7 +35,24 @@ export class PostApiServiceImpl implements PostApiService { throw new HttpError(response.status, url); } - const json = await response.json(); - return PostResponseDto.fromJson(json); + const data = await response.json(); + return PostResponseDto.fromJson(data); + } + + async createPost(payload: CreatePostRequestDto): Promise { + const url = new URL('post', Environment.API_BASE_URL); + + const response = await this.fetchFn(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload.toJson()), + }); + + if (!response.ok) { + throw new HttpError(response.status, url); + } + + const data = await response.json(); + return PostResponseDto.fromJson(data); } } diff --git a/frontend/src/lib/post/framework/ui/CreatePostDialog.svelte b/frontend/src/lib/post/framework/ui/CreatePostDialog.svelte new file mode 100644 index 0000000..57e4cb1 --- /dev/null +++ b/frontend/src/lib/post/framework/ui/CreatePostDialog.svelte @@ -0,0 +1,107 @@ + + + + + + Create + e.preventDefault()} + onEscapeKeydown={(e) => e.preventDefault()} + > + + Create Post + + + +
+ + + {#if formErrors.semanticId} +

{formErrors.semanticId}

+ {/if} +
+ +
+ + + {#if formErrors.title} +

{formErrors.title}

+ {/if} +
+ + + + + + +
+
diff --git a/frontend/src/lib/post/framework/ui/PostManagementPage.svelte b/frontend/src/lib/post/framework/ui/PostManagementPage.svelte new file mode 100644 index 0000000..e0970ce --- /dev/null +++ b/frontend/src/lib/post/framework/ui/PostManagementPage.svelte @@ -0,0 +1,35 @@ + + +
+
+

Post

+ +
+

+
diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte index 4d78c58..35d7061 100644 --- a/frontend/src/routes/+error.svelte +++ b/frontend/src/routes/+error.svelte @@ -1,4 +1,4 @@ - diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 3dfb295..cf92cb0 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,4 +1,4 @@ - diff --git a/frontend/src/routes/dashboard/image/+page.svelte b/frontend/src/routes/dashboard/image/+page.svelte index 5fc7351..c2ffd8b 100644 --- a/frontend/src/routes/dashboard/image/+page.svelte +++ b/frontend/src/routes/dashboard/image/+page.svelte @@ -1,18 +1,11 @@ diff --git a/frontend/src/routes/dashboard/post/+page.svelte b/frontend/src/routes/dashboard/post/+page.svelte index fc9a374..e3c8cf3 100644 --- a/frontend/src/routes/dashboard/post/+page.svelte +++ b/frontend/src/routes/dashboard/post/+page.svelte @@ -1 +1,12 @@ -
Post
+ + + -- 2.47.2 From df5bba022e3af886b17a1843aa7ff87a65a539b4 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Tue, 14 Oct 2025 21:27:48 +0800 Subject: [PATCH 04/10] fix: eslint --- frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts | 5 ++++- frontend/src/lib/post/adapter/presenter/postBloc.ts | 6 ++---- frontend/src/lib/post/framework/ui/CreatePostDialog.svelte | 2 +- .../src/lib/post/framework/ui/PostManagementPage.svelte | 3 +-- frontend/src/routes/dashboard/+layout.svelte | 2 +- frontend/src/routes/dashboard/post/+page.svelte | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts index 764b3cc..6a69c72 100644 --- a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts +++ b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts @@ -1,6 +1,9 @@ import { CreatePostRequestDto } from '$lib/post/adapter/gateway/creatPostRequestDto'; import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; -import type { CreatePostParams, PostRepository } from '$lib/post/application/gateway/postRepository'; +import type { + CreatePostParams, + PostRepository, +} from '$lib/post/application/gateway/postRepository'; import type { Post } from '$lib/post/domain/entity/post'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; diff --git a/frontend/src/lib/post/adapter/presenter/postBloc.ts b/frontend/src/lib/post/adapter/presenter/postBloc.ts index 35a6f92..1b867f7 100644 --- a/frontend/src/lib/post/adapter/presenter/postBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postBloc.ts @@ -38,8 +38,7 @@ export class PostBloc { case PostEventType.PostLoadedEvent: return this.loadPost(event.id); case PostEventType.PostCreatedEvent: - const { semanticId, title } = event; - return this.createPost({ semanticId, title }); + return this.createPost(event.params); } } @@ -95,6 +94,5 @@ interface PostLoadedEvent { interface PostCreatedEvent { event: PostEventType.PostCreatedEvent; - semanticId: string; - title: string; + params: CreatePostParams; } diff --git a/frontend/src/lib/post/framework/ui/CreatePostDialog.svelte b/frontend/src/lib/post/framework/ui/CreatePostDialog.svelte index 57e4cb1..7197cf3 100644 --- a/frontend/src/lib/post/framework/ui/CreatePostDialog.svelte +++ b/frontend/src/lib/post/framework/ui/CreatePostDialog.svelte @@ -6,7 +6,7 @@ .string() .max(100) .regex(/\D/) - .regex(/^[a-zA-Z0-9_\-]+$/), + .regex(/^[a-zA-Z0-9_-]+$/), title: z.string().trim().nonempty().max(100), }); diff --git a/frontend/src/lib/post/framework/ui/PostManagementPage.svelte b/frontend/src/lib/post/framework/ui/PostManagementPage.svelte index e0970ce..6acee72 100644 --- a/frontend/src/lib/post/framework/ui/PostManagementPage.svelte +++ b/frontend/src/lib/post/framework/ui/PostManagementPage.svelte @@ -1,5 +1,4 @@ -- 2.47.2 From 4c4c5c357eecdd5032371eb2614a08780ab96aac Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Tue, 14 Oct 2025 21:56:51 +0800 Subject: [PATCH 05/10] feat: make preview_image_url nullable across post-related DTOs and database schema --- .../delivery/create_post_request_dto.rs | 4 ++-- .../delivery/post_info_response_dto.rs | 2 +- .../delivery/update_post_request_dto.rs | 4 ++-- .../adapter/gateway/post_info_db_mapper.rs | 2 +- .../post/src/domain/entity/post_info.rs | 2 +- .../db/post_info_with_label_record.rs | 2 +- .../framework/db/post_with_label_record.rs | 2 +- ...9_make_preview_image_url_nullable.down.sql | 2 ++ ...133149_make_preview_image_url_nullable.sql | 2 ++ .../adapter/gateway/creatPostRequestDto.ts | 6 ++--- .../adapter/gateway/postInfoResponseDto.ts | 13 +++++++---- .../adapter/presenter/postInfoViewModel.ts | 15 ++++++++---- .../src/lib/post/domain/entity/postInfo.ts | 4 ++-- .../post/framework/ui/PostContentPage.svelte | 23 ++++++++++--------- .../lib/post/framework/ui/PostPreview.svelte | 10 ++++---- .../post/framework/ui/StructuredData.svelte | 4 ++-- 16 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 backend/migrations/20251014133149_make_preview_image_url_nullable.down.sql create mode 100644 backend/migrations/20251014133149_make_preview_image_url_nullable.sql diff --git a/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs index 874c059..3a2856b 100644 --- a/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs +++ b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs @@ -12,8 +12,8 @@ pub struct CreatePostRequestDto { pub content: String, pub label_ids: Vec, - #[schema(format = Uri)] - pub preview_image_url: String, + #[schema(required, format = Uri)] + pub preview_image_url: Option, #[schema(required, format = DateTime)] pub published_time: Option, diff --git a/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs b/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs index bb83498..7553423 100644 --- a/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs +++ b/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs @@ -14,7 +14,7 @@ pub struct PostInfoResponseDto { pub labels: Vec, #[schema(format = Uri)] - pub preview_image_url: String, + pub preview_image_url: Option, #[schema(format = DateTime)] pub published_time: Option, diff --git a/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs index 5f93c5f..b5e839b 100644 --- a/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs +++ b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs @@ -12,8 +12,8 @@ pub struct UpdatePostRequestDto { pub content: String, pub label_ids: Vec, - #[schema(format = Uri)] - pub preview_image_url: String, + #[schema(required, format = Uri)] + pub preview_image_url: Option, #[schema(required, format = DateTime)] pub published_time: Option, diff --git a/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs b/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs index 6663175..9478738 100644 --- a/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs +++ b/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs @@ -7,7 +7,7 @@ pub struct PostInfoMapper { pub semantic_id: String, pub title: String, pub description: String, - pub preview_image_url: String, + pub preview_image_url: Option, pub published_time: Option, pub labels: Vec, } diff --git a/backend/feature/post/src/domain/entity/post_info.rs b/backend/feature/post/src/domain/entity/post_info.rs index 922be11..3aebd5a 100644 --- a/backend/feature/post/src/domain/entity/post_info.rs +++ b/backend/feature/post/src/domain/entity/post_info.rs @@ -10,7 +10,7 @@ pub struct PostInfo { pub semantic_id: String, pub title: String, pub description: String, - pub preview_image_url: String, + pub preview_image_url: Option, pub labels: Vec