From 10b087c381116011499ca14511e2738081d50a3e Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Wed, 4 Mar 2026 11:01:02 +0900 Subject: [PATCH 01/28] build(gradle): add jpa, postgres, update application.yml --- build.gradle | 4 ++++ docker-compose.yml | 11 +++++++++++ src/main/resources/application-dev.yml | 17 ++++++++++++++++- src/main/resources/application.yml | 4 ---- 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 docker-compose.yml diff --git a/build.gradle b/build.gradle index d15f6e9d2..a57aebdd8 100644 --- a/build.gradle +++ b/build.gradle @@ -20,10 +20,14 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation('org.springframework.boot:spring-boot-starter-validation') implementation('org.springdoc:springdoc-openapi-starter-webmvc-ui') + implementation('org.springframework.boot:spring-boot-starter-data-jpa') compileOnly("org.projectlombok:lombok:1.18.42") annotationProcessor("org.projectlombok:lombok:1.18.42") + developmentOnly('org.springframework.boot:spring-boot-devtools') + runtimeOnly('org.postgresql:postgresql:42.7.7') + testCompileOnly("org.projectlombok:lombok:1.18.42") testAnnotationProcessor("org.projectlombok:lombok:1.18.42") testImplementation('org.springframework.boot:spring-boot-starter-test') diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..68b379b6d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + db: + image: postgres + container_name: discodeit-A + restart: always + environment: + POSTGRES_USER: jonas + POSTGRES_PASSWORD: jonas + POSTGRES_DB: discodeit + ports: + - "5432:5432" diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6e5fadeb0..70bceda66 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,4 +3,19 @@ discodeit: type: jcf # jcf | file file-directory: .discodeit uuid: - type: dev # dev / prod \ No newline at end of file + type: dev # dev / prod + +springdoc: + swagger-ui: + url: /api-docs_1.1.json + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/discodeit + username: jonas + password: jonas + jpa: + hibernate: + ddl-auto: create + show-sql: true + database: PostgreSQL \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index da07248bc..52af7394c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,3 @@ spring: profiles: active: dev # dev | prod - -springdoc: - swagger-ui: - url: /api-docs.json \ No newline at end of file From d732a2264d81ee0aa28fcbd441e6cef46939c919 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Wed, 4 Mar 2026 11:03:18 +0900 Subject: [PATCH 02/28] chore(resources.static): add new api specs, assets --- .../resources/static/{ => api}/api-docs.json | 0 .../resources/static/api/api-docs_1.1.json | 1269 +++++++++++++++++ .../resources/static/assets/index-D8OMG6Bz.js | 1015 +++++++++++++ .../static/assets/index-kQJbKSsj.css | 1 + src/main/resources/static/favicon.ico | Bin 0 -> 1588 bytes src/main/resources/static/index.html | 26 + 6 files changed, 2311 insertions(+) rename src/main/resources/static/{ => api}/api-docs.json (100%) create mode 100644 src/main/resources/static/api/api-docs_1.1.json create mode 100644 src/main/resources/static/assets/index-D8OMG6Bz.js create mode 100644 src/main/resources/static/assets/index-kQJbKSsj.css create mode 100644 src/main/resources/static/favicon.ico create mode 100644 src/main/resources/static/index.html diff --git a/src/main/resources/static/api-docs.json b/src/main/resources/static/api/api-docs.json similarity index 100% rename from src/main/resources/static/api-docs.json rename to src/main/resources/static/api/api-docs.json diff --git a/src/main/resources/static/api/api-docs_1.1.json b/src/main/resources/static/api/api-docs_1.1.json new file mode 100644 index 000000000..4c25e0983 --- /dev/null +++ b/src/main/resources/static/api/api-docs_1.1.json @@ -0,0 +1,1269 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Discodeit API 문서", + "description": "Discodeit 프로젝트의 Swagger API 문서입니다." + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "로컬 서버" + } + ], + "tags": [ + { + "name": "Channel", + "description": "Channel API" + }, + { + "name": "ReadStatus", + "description": "Message 읽음 상태 API" + }, + { + "name": "Message", + "description": "Message API" + }, + { + "name": "User", + "description": "User API" + }, + { + "name": "BinaryContent", + "description": "첨부 파일 API" + }, + { + "name": "Auth", + "description": "인증 API" + } + ], + "paths": { + "/api/users": { + "get": { + "tags": [ + "User" + ], + "summary": "전체 User 목록 조회", + "operationId": "findAll", + "responses": { + "200": { + "description": "User 목록 조회 성공", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "User" + ], + "summary": "User 등록", + "operationId": "create", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "userCreateRequest": { + "$ref": "#/components/schemas/UserCreateRequest" + }, + "profile": { + "type": "string", + "format": "binary", + "description": "User 프로필 이미지" + } + }, + "required": [ + "userCreateRequest" + ] + } + } + } + }, + "responses": { + "400": { + "description": "같은 email 또는 username를 사용하는 User가 이미 존재함", + "content": { + "*/*": { + "example": "User with email {email} already exists" + } + } + }, + "201": { + "description": "User가 성공적으로 생성됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + } + } + } + }, + "/api/readStatuses": { + "get": { + "tags": [ + "ReadStatus" + ], + "summary": "User의 Message 읽음 상태 목록 조회", + "operationId": "findAllByUserId", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "조회할 User ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Message 읽음 상태 목록 조회 성공", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReadStatusDto" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "ReadStatus" + ], + "summary": "Message 읽음 상태 생성", + "operationId": "create_1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadStatusCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "이미 읽음 상태가 존재함", + "content": { + "*/*": { + "example": "ReadStatus with userId {userId} and channelId {channelId} already exists" + } + } + }, + "201": { + "description": "Message 읽음 상태가 성공적으로 생성됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ReadStatusDto" + } + } + } + }, + "404": { + "description": "Channel 또는 User를 찾을 수 없음", + "content": { + "*/*": { + "example": "Channel | User with id {channelId | userId} not found" + } + } + } + } + } + }, + "/api/messages": { + "get": { + "tags": [ + "Message" + ], + "summary": "Channel의 Message 목록 조회", + "operationId": "findAllByChannelId", + "parameters": [ + { + "name": "channelId", + "in": "query", + "description": "조회할 Channel ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "pageable", + "in": "query", + "description": "페이징 정보", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + }, + "example": { + "size": 50, + "page": 0, + "sort": "createdAt,desc" + } + } + ], + "responses": { + "200": { + "description": "Message 목록 조회 성공", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Message" + ], + "summary": "Message 생성", + "operationId": "create_2", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "messageCreateRequest": { + "$ref": "#/components/schemas/MessageCreateRequest" + }, + "attachments": { + "type": "array", + "description": "Message 첨부 파일들", + "items": { + "type": "string", + "format": "binary" + } + } + }, + "required": [ + "messageCreateRequest" + ] + } + } + } + }, + "responses": { + "404": { + "description": "Channel 또는 User를 찾을 수 없음", + "content": { + "*/*": { + "example": "Channel | Author with id {channelId | author} not found" + } + } + }, + "201": { + "description": "Message가 성공적으로 생성됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/MessageDto" + } + } + } + } + } + } + }, + "/api/channels/public": { + "post": { + "tags": [ + "Channel" + ], + "summary": "Public Channel 생성", + "operationId": "create_3", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicChannelCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Public Channel이 성공적으로 생성됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ChannelDto" + } + } + } + } + } + } + }, + "/api/channels/private": { + "post": { + "tags": [ + "Channel" + ], + "summary": "Private Channel 생성", + "operationId": "create_4", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateChannelCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Private Channel이 성공적으로 생성됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ChannelDto" + } + } + } + } + } + } + }, + "/api/auth/login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "로그인", + "operationId": "login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "404": { + "description": "사용자를 찾을 수 없음", + "content": { + "*/*": { + "example": "User with username {username} not found" + } + } + }, + "400": { + "description": "비밀번호가 일치하지 않음", + "content": { + "*/*": { + "example": "Wrong password" + } + } + }, + "200": { + "description": "로그인 성공", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + } + } + } + }, + "/api/users/{userId}": { + "delete": { + "tags": [ + "User" + ], + "summary": "User 삭제", + "operationId": "delete", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "삭제할 User ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "404": { + "description": "User를 찾을 수 없음", + "content": { + "*/*": { + "example": "User with id {id} not found" + } + } + }, + "204": { + "description": "User가 성공적으로 삭제됨" + } + } + }, + "patch": { + "tags": [ + "User" + ], + "summary": "User 정보 수정", + "operationId": "update", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "수정할 User ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "userUpdateRequest": { + "$ref": "#/components/schemas/UserUpdateRequest" + }, + "profile": { + "type": "string", + "format": "binary", + "description": "수정할 User 프로필 이미지" + } + }, + "required": [ + "userUpdateRequest" + ] + } + } + } + }, + "responses": { + "200": { + "description": "User 정보가 성공적으로 수정됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + }, + "400": { + "description": "같은 email 또는 username를 사용하는 User가 이미 존재함", + "content": { + "*/*": { + "example": "user with email {newEmail} already exists" + } + } + }, + "404": { + "description": "User를 찾을 수 없음", + "content": { + "*/*": { + "example": "User with id {userId} not found" + } + } + } + } + } + }, + "/api/users/{userId}/userStatus": { + "patch": { + "tags": [ + "User" + ], + "summary": "User 온라인 상태 업데이트", + "operationId": "updateUserStatusByUserId", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "상태를 변경할 User ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserStatusUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "404": { + "description": "해당 User의 UserStatus를 찾을 수 없음", + "content": { + "*/*": { + "example": "UserStatus with userId {userId} not found" + } + } + }, + "200": { + "description": "User 온라인 상태가 성공적으로 업데이트됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserStatusDto" + } + } + } + } + } + } + }, + "/api/readStatuses/{readStatusId}": { + "patch": { + "tags": [ + "ReadStatus" + ], + "summary": "Message 읽음 상태 수정", + "operationId": "update_1", + "parameters": [ + { + "name": "readStatusId", + "in": "path", + "description": "수정할 읽음 상태 ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadStatusUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Message 읽음 상태가 성공적으로 수정됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ReadStatusDto" + } + } + } + }, + "404": { + "description": "Message 읽음 상태를 찾을 수 없음", + "content": { + "*/*": { + "example": "ReadStatus with id {readStatusId} not found" + } + } + } + } + } + }, + "/api/messages/{messageId}": { + "delete": { + "tags": [ + "Message" + ], + "summary": "Message 삭제", + "operationId": "delete_1", + "parameters": [ + { + "name": "messageId", + "in": "path", + "description": "삭제할 Message ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Message가 성공적으로 삭제됨" + }, + "404": { + "description": "Message를 찾을 수 없음", + "content": { + "*/*": { + "example": "Message with id {messageId} not found" + } + } + } + } + }, + "patch": { + "tags": [ + "Message" + ], + "summary": "Message 내용 수정", + "operationId": "update_2", + "parameters": [ + { + "name": "messageId", + "in": "path", + "description": "수정할 Message ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Message가 성공적으로 수정됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/MessageDto" + } + } + } + }, + "404": { + "description": "Message를 찾을 수 없음", + "content": { + "*/*": { + "example": "Message with id {messageId} not found" + } + } + } + } + } + }, + "/api/channels/{channelId}": { + "delete": { + "tags": [ + "Channel" + ], + "summary": "Channel 삭제", + "operationId": "delete_2", + "parameters": [ + { + "name": "channelId", + "in": "path", + "description": "삭제할 Channel ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Channel이 성공적으로 삭제됨" + }, + "404": { + "description": "Channel을 찾을 수 없음", + "content": { + "*/*": { + "example": "Channel with id {channelId} not found" + } + } + } + } + }, + "patch": { + "tags": [ + "Channel" + ], + "summary": "Channel 정보 수정", + "operationId": "update_3", + "parameters": [ + { + "name": "channelId", + "in": "path", + "description": "수정할 Channel ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicChannelUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Channel 정보가 성공적으로 수정됨", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ChannelDto" + } + } + } + }, + "404": { + "description": "Channel을 찾을 수 없음", + "content": { + "*/*": { + "example": "Channel with id {channelId} not found" + } + } + }, + "400": { + "description": "Private Channel은 수정할 수 없음", + "content": { + "*/*": { + "example": "Private channel cannot be updated" + } + } + } + } + } + }, + "/api/channels": { + "get": { + "tags": [ + "Channel" + ], + "summary": "User가 참여 중인 Channel 목록 조회", + "operationId": "findAll_1", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "조회할 User ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Channel 목록 조회 성공", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelDto" + } + } + } + } + } + } + } + }, + "/api/binaryContents": { + "get": { + "tags": [ + "BinaryContent" + ], + "summary": "여러 첨부 파일 조회", + "operationId": "findAllByIdIn", + "parameters": [ + { + "name": "binaryContentIds", + "in": "query", + "description": "조회할 첨부 파일 ID 목록", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "200": { + "description": "첨부 파일 목록 조회 성공", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BinaryContentDto" + } + } + } + } + } + } + } + }, + "/api/binaryContents/{binaryContentId}": { + "get": { + "tags": [ + "BinaryContent" + ], + "summary": "첨부 파일 조회", + "operationId": "find", + "parameters": [ + { + "name": "binaryContentId", + "in": "path", + "description": "조회할 첨부 파일 ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "첨부 파일 조회 성공", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BinaryContentDto" + } + } + } + }, + "404": { + "description": "첨부 파일을 찾을 수 없음", + "content": { + "*/*": { + "example": "BinaryContent with id {binaryContentId} not found" + } + } + } + } + } + }, + "/api/binaryContents/{binaryContentId}/download": { + "get": { + "tags": [ + "BinaryContent" + ], + "summary": "파일 다운로드", + "operationId": "download", + "parameters": [ + { + "name": "binaryContentId", + "in": "path", + "description": "다운로드할 파일 ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "파일 다운로드 성공", + "content": { + "*/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserCreateRequest": { + "type": "object", + "description": "User 생성 정보", + "properties": { + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "BinaryContentDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "fileName": { + "type": "string" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "contentType": { + "type": "string" + } + } + }, + "UserDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "profile": { + "$ref": "#/components/schemas/BinaryContentDto" + }, + "online": { + "type": "boolean" + } + } + }, + "ReadStatusCreateRequest": { + "type": "object", + "description": "Message 읽음 상태 생성 정보", + "properties": { + "userId": { + "type": "string", + "format": "uuid" + }, + "channelId": { + "type": "string", + "format": "uuid" + }, + "lastReadAt": { + "type": "string", + "format": "date-time" + } + } + }, + "ReadStatusDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "channelId": { + "type": "string", + "format": "uuid" + }, + "lastReadAt": { + "type": "string", + "format": "date-time" + } + } + }, + "MessageCreateRequest": { + "type": "object", + "description": "Message 생성 정보", + "properties": { + "content": { + "type": "string" + }, + "channelId": { + "type": "string", + "format": "uuid" + }, + "authorId": { + "type": "string", + "format": "uuid" + } + } + }, + "MessageDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "content": { + "type": "string" + }, + "channelId": { + "type": "string", + "format": "uuid" + }, + "author": { + "$ref": "#/components/schemas/UserDto" + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BinaryContentDto" + } + } + } + }, + "PublicChannelCreateRequest": { + "type": "object", + "description": "Public Channel 생성 정보", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "ChannelDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "PUBLIC", + "PRIVATE" + ] + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + }, + "lastMessageAt": { + "type": "string", + "format": "date-time" + } + } + }, + "PrivateChannelCreateRequest": { + "type": "object", + "description": "Private Channel 생성 정보", + "properties": { + "participantIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + }, + "LoginRequest": { + "type": "object", + "description": "로그인 정보", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "UserUpdateRequest": { + "type": "object", + "description": "수정할 User 정보", + "properties": { + "newUsername": { + "type": "string" + }, + "newEmail": { + "type": "string" + }, + "newPassword": { + "type": "string" + } + } + }, + "UserStatusUpdateRequest": { + "type": "object", + "description": "변경할 User 온라인 상태 정보", + "properties": { + "newLastActiveAt": { + "type": "string", + "format": "date-time" + } + } + }, + "UserStatusDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "lastActiveAt": { + "type": "string", + "format": "date-time" + } + } + }, + "ReadStatusUpdateRequest": { + "type": "object", + "description": "수정할 읽음 상태 정보", + "properties": { + "newLastReadAt": { + "type": "string", + "format": "date-time" + } + } + }, + "MessageUpdateRequest": { + "type": "object", + "description": "수정할 Message 내용", + "properties": { + "newContent": { + "type": "string" + } + } + }, + "PublicChannelUpdateRequest": { + "type": "object", + "description": "수정할 Channel 정보", + "properties": { + "newName": { + "type": "string" + }, + "newDescription": { + "type": "string" + } + } + }, + "Pageable": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "size": { + "type": "integer", + "format": "int32", + "minimum": 1 + }, + "sort": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PageResponse": { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "type": "object" + } + }, + "number": { + "type": "integer", + "format": "int32" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "hasNext": { + "type": "boolean" + }, + "totalElements": { + "type": "integer", + "format": "int64" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/static/assets/index-D8OMG6Bz.js b/src/main/resources/static/assets/index-D8OMG6Bz.js new file mode 100644 index 000000000..84f4ce135 --- /dev/null +++ b/src/main/resources/static/assets/index-D8OMG6Bz.js @@ -0,0 +1,1015 @@ +var rg=Object.defineProperty;var og=(r,i,s)=>i in r?rg(r,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):r[i]=s;var ed=(r,i,s)=>og(r,typeof i!="symbol"?i+"":i,s);(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const c of document.querySelectorAll('link[rel="modulepreload"]'))l(c);new MutationObserver(c=>{for(const f of c)if(f.type==="childList")for(const p of f.addedNodes)p.tagName==="LINK"&&p.rel==="modulepreload"&&l(p)}).observe(document,{childList:!0,subtree:!0});function s(c){const f={};return c.integrity&&(f.integrity=c.integrity),c.referrerPolicy&&(f.referrerPolicy=c.referrerPolicy),c.crossOrigin==="use-credentials"?f.credentials="include":c.crossOrigin==="anonymous"?f.credentials="omit":f.credentials="same-origin",f}function l(c){if(c.ep)return;c.ep=!0;const f=s(c);fetch(c.href,f)}})();function ig(r){return r&&r.__esModule&&Object.prototype.hasOwnProperty.call(r,"default")?r.default:r}var mu={exports:{}},yo={},gu={exports:{}},fe={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var td;function sg(){if(td)return fe;td=1;var r=Symbol.for("react.element"),i=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),l=Symbol.for("react.strict_mode"),c=Symbol.for("react.profiler"),f=Symbol.for("react.provider"),p=Symbol.for("react.context"),g=Symbol.for("react.forward_ref"),x=Symbol.for("react.suspense"),v=Symbol.for("react.memo"),S=Symbol.for("react.lazy"),A=Symbol.iterator;function R(E){return E===null||typeof E!="object"?null:(E=A&&E[A]||E["@@iterator"],typeof E=="function"?E:null)}var I={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,C={};function O(E,D,se){this.props=E,this.context=D,this.refs=C,this.updater=se||I}O.prototype.isReactComponent={},O.prototype.setState=function(E,D){if(typeof E!="object"&&typeof E!="function"&&E!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,E,D,"setState")},O.prototype.forceUpdate=function(E){this.updater.enqueueForceUpdate(this,E,"forceUpdate")};function F(){}F.prototype=O.prototype;function B(E,D,se){this.props=E,this.context=D,this.refs=C,this.updater=se||I}var V=B.prototype=new F;V.constructor=B,_(V,O.prototype),V.isPureReactComponent=!0;var Q=Array.isArray,H=Object.prototype.hasOwnProperty,L={current:null},b={key:!0,ref:!0,__self:!0,__source:!0};function re(E,D,se){var ue,de={},ce=null,ve=null;if(D!=null)for(ue in D.ref!==void 0&&(ve=D.ref),D.key!==void 0&&(ce=""+D.key),D)H.call(D,ue)&&!b.hasOwnProperty(ue)&&(de[ue]=D[ue]);var pe=arguments.length-2;if(pe===1)de.children=se;else if(1>>1,D=W[E];if(0>>1;Ec(de,Y))cec(ve,de)?(W[E]=ve,W[ce]=Y,E=ce):(W[E]=de,W[ue]=Y,E=ue);else if(cec(ve,Y))W[E]=ve,W[ce]=Y,E=ce;else break e}}return Z}function c(W,Z){var Y=W.sortIndex-Z.sortIndex;return Y!==0?Y:W.id-Z.id}if(typeof performance=="object"&&typeof performance.now=="function"){var f=performance;r.unstable_now=function(){return f.now()}}else{var p=Date,g=p.now();r.unstable_now=function(){return p.now()-g}}var x=[],v=[],S=1,A=null,R=3,I=!1,_=!1,C=!1,O=typeof setTimeout=="function"?setTimeout:null,F=typeof clearTimeout=="function"?clearTimeout:null,B=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function V(W){for(var Z=s(v);Z!==null;){if(Z.callback===null)l(v);else if(Z.startTime<=W)l(v),Z.sortIndex=Z.expirationTime,i(x,Z);else break;Z=s(v)}}function Q(W){if(C=!1,V(W),!_)if(s(x)!==null)_=!0,We(H);else{var Z=s(v);Z!==null&&Se(Q,Z.startTime-W)}}function H(W,Z){_=!1,C&&(C=!1,F(re),re=-1),I=!0;var Y=R;try{for(V(Z),A=s(x);A!==null&&(!(A.expirationTime>Z)||W&&!at());){var E=A.callback;if(typeof E=="function"){A.callback=null,R=A.priorityLevel;var D=E(A.expirationTime<=Z);Z=r.unstable_now(),typeof D=="function"?A.callback=D:A===s(x)&&l(x),V(Z)}else l(x);A=s(x)}if(A!==null)var se=!0;else{var ue=s(v);ue!==null&&Se(Q,ue.startTime-Z),se=!1}return se}finally{A=null,R=Y,I=!1}}var L=!1,b=null,re=-1,ye=5,Ne=-1;function at(){return!(r.unstable_now()-NeW||125E?(W.sortIndex=Y,i(v,W),s(x)===null&&W===s(v)&&(C?(F(re),re=-1):C=!0,Se(Q,Y-E))):(W.sortIndex=D,i(x,W),_||I||(_=!0,We(H))),W},r.unstable_shouldYield=at,r.unstable_wrapCallback=function(W){var Z=R;return function(){var Y=R;R=Z;try{return W.apply(this,arguments)}finally{R=Y}}}}(wu)),wu}var sd;function cg(){return sd||(sd=1,vu.exports=ag()),vu.exports}/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ld;function fg(){if(ld)return lt;ld=1;var r=Ku(),i=cg();function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),x=Object.prototype.hasOwnProperty,v=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,S={},A={};function R(e){return x.call(A,e)?!0:x.call(S,e)?!1:v.test(e)?A[e]=!0:(S[e]=!0,!1)}function I(e,t,n,o){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return o?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function _(e,t,n,o){if(t===null||typeof t>"u"||I(e,t,n,o))return!0;if(o)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function C(e,t,n,o,u,a,d){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=o,this.attributeNamespace=u,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=d}var O={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){O[e]=new C(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];O[t]=new C(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){O[e]=new C(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){O[e]=new C(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){O[e]=new C(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){O[e]=new C(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){O[e]=new C(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){O[e]=new C(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){O[e]=new C(e,5,!1,e.toLowerCase(),null,!1,!1)});var F=/[\-:]([a-z])/g;function B(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(F,B);O[t]=new C(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(F,B);O[t]=new C(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(F,B);O[t]=new C(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){O[e]=new C(e,1,!1,e.toLowerCase(),null,!1,!1)}),O.xlinkHref=new C("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){O[e]=new C(e,1,!1,e.toLowerCase(),null,!0,!0)});function V(e,t,n,o){var u=O.hasOwnProperty(t)?O[t]:null;(u!==null?u.type!==0:o||!(2m||u[d]!==a[m]){var y=` +`+u[d].replace(" at new "," at ");return e.displayName&&y.includes("")&&(y=y.replace("",e.displayName)),y}while(1<=d&&0<=m);break}}}finally{se=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?D(e):""}function de(e){switch(e.tag){case 5:return D(e.type);case 16:return D("Lazy");case 13:return D("Suspense");case 19:return D("SuspenseList");case 0:case 2:case 15:return e=ue(e.type,!1),e;case 11:return e=ue(e.type.render,!1),e;case 1:return e=ue(e.type,!0),e;default:return""}}function ce(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case b:return"Fragment";case L:return"Portal";case ye:return"Profiler";case re:return"StrictMode";case Ze:return"Suspense";case ct:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case at:return(e.displayName||"Context")+".Consumer";case Ne:return(e._context.displayName||"Context")+".Provider";case wt:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case xt:return t=e.displayName||null,t!==null?t:ce(e.type)||"Memo";case We:t=e._payload,e=e._init;try{return ce(e(t))}catch{}}return null}function ve(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ce(t);case 8:return t===re?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function pe(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function me(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function He(e){var t=me(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),o=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var u=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return u.call(this)},set:function(d){o=""+d,a.call(this,d)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return o},setValue:function(d){o=""+d},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Yt(e){e._valueTracker||(e._valueTracker=He(e))}function Pt(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),o="";return e&&(o=me(e)?e.checked?"true":"false":e.value),e=o,e!==n?(t.setValue(e),!0):!1}function No(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Es(e,t){var n=t.checked;return Y({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function sa(e,t){var n=t.defaultValue==null?"":t.defaultValue,o=t.checked!=null?t.checked:t.defaultChecked;n=pe(t.value!=null?t.value:n),e._wrapperState={initialChecked:o,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function la(e,t){t=t.checked,t!=null&&V(e,"checked",t,!1)}function Cs(e,t){la(e,t);var n=pe(t.value),o=t.type;if(n!=null)o==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(o==="submit"||o==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ks(e,t.type,n):t.hasOwnProperty("defaultValue")&&ks(e,t.type,pe(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ua(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var o=t.type;if(!(o!=="submit"&&o!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ks(e,t,n){(t!=="number"||No(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Ir=Array.isArray;function qn(e,t,n,o){if(e=e.options,t){t={};for(var u=0;u"+t.valueOf().toString()+"",t=Oo.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Nr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Or={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},uh=["Webkit","ms","Moz","O"];Object.keys(Or).forEach(function(e){uh.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Or[t]=Or[e]})});function ha(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Or.hasOwnProperty(e)&&Or[e]?(""+t).trim():t+"px"}function ma(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var o=n.indexOf("--")===0,u=ha(n,t[n],o);n==="float"&&(n="cssFloat"),o?e.setProperty(n,u):e[n]=u}}var ah=Y({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Rs(e,t){if(t){if(ah[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(s(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(s(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(s(61))}if(t.style!=null&&typeof t.style!="object")throw Error(s(62))}}function Ps(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var _s=null;function Ts(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Is=null,Qn=null,Gn=null;function ga(e){if(e=to(e)){if(typeof Is!="function")throw Error(s(280));var t=e.stateNode;t&&(t=ni(t),Is(e.stateNode,e.type,t))}}function ya(e){Qn?Gn?Gn.push(e):Gn=[e]:Qn=e}function va(){if(Qn){var e=Qn,t=Gn;if(Gn=Qn=null,ga(e),t)for(e=0;e>>=0,e===0?32:31-(xh(e)/Sh|0)|0}var Uo=64,Fo=4194304;function zr(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Bo(e,t){var n=e.pendingLanes;if(n===0)return 0;var o=0,u=e.suspendedLanes,a=e.pingedLanes,d=n&268435455;if(d!==0){var m=d&~u;m!==0?o=zr(m):(a&=d,a!==0&&(o=zr(a)))}else d=n&~u,d!==0?o=zr(d):a!==0&&(o=zr(a));if(o===0)return 0;if(t!==0&&t!==o&&!(t&u)&&(u=o&-o,a=t&-t,u>=a||u===16&&(a&4194240)!==0))return t;if(o&4&&(o|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=o;0n;n++)t.push(e);return t}function Ur(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-_t(t),e[t]=n}function jh(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var o=e.eventTimes;for(e=e.expirationTimes;0=Yr),Ya=" ",qa=!1;function Qa(e,t){switch(e){case"keyup":return Zh.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ga(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Jn=!1;function tm(e,t){switch(e){case"compositionend":return Ga(t);case"keypress":return t.which!==32?null:(qa=!0,Ya);case"textInput":return e=t.data,e===Ya&&qa?null:e;default:return null}}function nm(e,t){if(Jn)return e==="compositionend"||!Gs&&Qa(e,t)?(e=Ba(),Wo=bs=an=null,Jn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=o}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=nc(n)}}function oc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?oc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ic(){for(var e=window,t=No();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=No(e.document)}return t}function Js(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function fm(e){var t=ic(),n=e.focusedElem,o=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&oc(n.ownerDocument.documentElement,n)){if(o!==null&&Js(n)){if(t=o.start,e=o.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var u=n.textContent.length,a=Math.min(o.start,u);o=o.end===void 0?a:Math.min(o.end,u),!e.extend&&a>o&&(u=o,o=a,a=u),u=rc(n,a);var d=rc(n,o);u&&d&&(e.rangeCount!==1||e.anchorNode!==u.node||e.anchorOffset!==u.offset||e.focusNode!==d.node||e.focusOffset!==d.offset)&&(t=t.createRange(),t.setStart(u.node,u.offset),e.removeAllRanges(),a>o?(e.addRange(t),e.extend(d.node,d.offset)):(t.setEnd(d.node,d.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Zn=null,Zs=null,Kr=null,el=!1;function sc(e,t,n){var o=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;el||Zn==null||Zn!==No(o)||(o=Zn,"selectionStart"in o&&Js(o)?o={start:o.selectionStart,end:o.selectionEnd}:(o=(o.ownerDocument&&o.ownerDocument.defaultView||window).getSelection(),o={anchorNode:o.anchorNode,anchorOffset:o.anchorOffset,focusNode:o.focusNode,focusOffset:o.focusOffset}),Kr&&Gr(Kr,o)||(Kr=o,o=Zo(Zs,"onSelect"),0or||(e.current=dl[or],dl[or]=null,or--)}function Ee(e,t){or++,dl[or]=e.current,e.current=t}var pn={},Ye=dn(pn),nt=dn(!1),Rn=pn;function ir(e,t){var n=e.type.contextTypes;if(!n)return pn;var o=e.stateNode;if(o&&o.__reactInternalMemoizedUnmaskedChildContext===t)return o.__reactInternalMemoizedMaskedChildContext;var u={},a;for(a in n)u[a]=t[a];return o&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=u),u}function rt(e){return e=e.childContextTypes,e!=null}function ri(){ke(nt),ke(Ye)}function Sc(e,t,n){if(Ye.current!==pn)throw Error(s(168));Ee(Ye,t),Ee(nt,n)}function Ec(e,t,n){var o=e.stateNode;if(t=t.childContextTypes,typeof o.getChildContext!="function")return n;o=o.getChildContext();for(var u in o)if(!(u in t))throw Error(s(108,ve(e)||"Unknown",u));return Y({},n,o)}function oi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||pn,Rn=Ye.current,Ee(Ye,e),Ee(nt,nt.current),!0}function Cc(e,t,n){var o=e.stateNode;if(!o)throw Error(s(169));n?(e=Ec(e,t,Rn),o.__reactInternalMemoizedMergedChildContext=e,ke(nt),ke(Ye),Ee(Ye,e)):ke(nt),Ee(nt,n)}var Qt=null,ii=!1,pl=!1;function kc(e){Qt===null?Qt=[e]:Qt.push(e)}function Cm(e){ii=!0,kc(e)}function hn(){if(!pl&&Qt!==null){pl=!0;var e=0,t=xe;try{var n=Qt;for(xe=1;e>=d,u-=d,Gt=1<<32-_t(t)+u|n<oe?(Be=ne,ne=null):Be=ne.sibling;var ge=M(k,ne,j[oe],$);if(ge===null){ne===null&&(ne=Be);break}e&&ne&&ge.alternate===null&&t(k,ne),w=a(ge,w,oe),te===null?J=ge:te.sibling=ge,te=ge,ne=Be}if(oe===j.length)return n(k,ne),Ae&&_n(k,oe),J;if(ne===null){for(;oeoe?(Be=ne,ne=null):Be=ne.sibling;var Cn=M(k,ne,ge.value,$);if(Cn===null){ne===null&&(ne=Be);break}e&&ne&&Cn.alternate===null&&t(k,ne),w=a(Cn,w,oe),te===null?J=Cn:te.sibling=Cn,te=Cn,ne=Be}if(ge.done)return n(k,ne),Ae&&_n(k,oe),J;if(ne===null){for(;!ge.done;oe++,ge=j.next())ge=U(k,ge.value,$),ge!==null&&(w=a(ge,w,oe),te===null?J=ge:te.sibling=ge,te=ge);return Ae&&_n(k,oe),J}for(ne=o(k,ne);!ge.done;oe++,ge=j.next())ge=q(ne,k,oe,ge.value,$),ge!==null&&(e&&ge.alternate!==null&&ne.delete(ge.key===null?oe:ge.key),w=a(ge,w,oe),te===null?J=ge:te.sibling=ge,te=ge);return e&&ne.forEach(function(ng){return t(k,ng)}),Ae&&_n(k,oe),J}function Ie(k,w,j,$){if(typeof j=="object"&&j!==null&&j.type===b&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case H:e:{for(var J=j.key,te=w;te!==null;){if(te.key===J){if(J=j.type,J===b){if(te.tag===7){n(k,te.sibling),w=u(te,j.props.children),w.return=k,k=w;break e}}else if(te.elementType===J||typeof J=="object"&&J!==null&&J.$$typeof===We&&Tc(J)===te.type){n(k,te.sibling),w=u(te,j.props),w.ref=no(k,te,j),w.return=k,k=w;break e}n(k,te);break}else t(k,te);te=te.sibling}j.type===b?(w=zn(j.props.children,k.mode,$,j.key),w.return=k,k=w):($=Oi(j.type,j.key,j.props,null,k.mode,$),$.ref=no(k,w,j),$.return=k,k=$)}return d(k);case L:e:{for(te=j.key;w!==null;){if(w.key===te)if(w.tag===4&&w.stateNode.containerInfo===j.containerInfo&&w.stateNode.implementation===j.implementation){n(k,w.sibling),w=u(w,j.children||[]),w.return=k,k=w;break e}else{n(k,w);break}else t(k,w);w=w.sibling}w=cu(j,k.mode,$),w.return=k,k=w}return d(k);case We:return te=j._init,Ie(k,w,te(j._payload),$)}if(Ir(j))return K(k,w,j,$);if(Z(j))return X(k,w,j,$);ai(k,j)}return typeof j=="string"&&j!==""||typeof j=="number"?(j=""+j,w!==null&&w.tag===6?(n(k,w.sibling),w=u(w,j),w.return=k,k=w):(n(k,w),w=au(j,k.mode,$),w.return=k,k=w),d(k)):n(k,w)}return Ie}var ar=Ic(!0),Nc=Ic(!1),ci=dn(null),fi=null,cr=null,wl=null;function xl(){wl=cr=fi=null}function Sl(e){var t=ci.current;ke(ci),e._currentValue=t}function El(e,t,n){for(;e!==null;){var o=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,o!==null&&(o.childLanes|=t)):o!==null&&(o.childLanes&t)!==t&&(o.childLanes|=t),e===n)break;e=e.return}}function fr(e,t){fi=e,wl=cr=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(ot=!0),e.firstContext=null)}function Ct(e){var t=e._currentValue;if(wl!==e)if(e={context:e,memoizedValue:t,next:null},cr===null){if(fi===null)throw Error(s(308));cr=e,fi.dependencies={lanes:0,firstContext:e}}else cr=cr.next=e;return t}var Tn=null;function Cl(e){Tn===null?Tn=[e]:Tn.push(e)}function Oc(e,t,n,o){var u=t.interleaved;return u===null?(n.next=n,Cl(t)):(n.next=u.next,u.next=n),t.interleaved=n,Xt(e,o)}function Xt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var mn=!1;function kl(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Lc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Jt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function gn(e,t,n){var o=e.updateQueue;if(o===null)return null;if(o=o.shared,he&2){var u=o.pending;return u===null?t.next=t:(t.next=u.next,u.next=t),o.pending=t,Xt(e,n)}return u=o.interleaved,u===null?(t.next=t,Cl(o)):(t.next=u.next,u.next=t),o.interleaved=t,Xt(e,n)}function di(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var o=t.lanes;o&=e.pendingLanes,n|=o,t.lanes=n,Us(e,n)}}function Dc(e,t){var n=e.updateQueue,o=e.alternate;if(o!==null&&(o=o.updateQueue,n===o)){var u=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var d={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?u=a=d:a=a.next=d,n=n.next}while(n!==null);a===null?u=a=t:a=a.next=t}else u=a=t;n={baseState:o.baseState,firstBaseUpdate:u,lastBaseUpdate:a,shared:o.shared,effects:o.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function pi(e,t,n,o){var u=e.updateQueue;mn=!1;var a=u.firstBaseUpdate,d=u.lastBaseUpdate,m=u.shared.pending;if(m!==null){u.shared.pending=null;var y=m,P=y.next;y.next=null,d===null?a=P:d.next=P,d=y;var z=e.alternate;z!==null&&(z=z.updateQueue,m=z.lastBaseUpdate,m!==d&&(m===null?z.firstBaseUpdate=P:m.next=P,z.lastBaseUpdate=y))}if(a!==null){var U=u.baseState;d=0,z=P=y=null,m=a;do{var M=m.lane,q=m.eventTime;if((o&M)===M){z!==null&&(z=z.next={eventTime:q,lane:0,tag:m.tag,payload:m.payload,callback:m.callback,next:null});e:{var K=e,X=m;switch(M=t,q=n,X.tag){case 1:if(K=X.payload,typeof K=="function"){U=K.call(q,U,M);break e}U=K;break e;case 3:K.flags=K.flags&-65537|128;case 0:if(K=X.payload,M=typeof K=="function"?K.call(q,U,M):K,M==null)break e;U=Y({},U,M);break e;case 2:mn=!0}}m.callback!==null&&m.lane!==0&&(e.flags|=64,M=u.effects,M===null?u.effects=[m]:M.push(m))}else q={eventTime:q,lane:M,tag:m.tag,payload:m.payload,callback:m.callback,next:null},z===null?(P=z=q,y=U):z=z.next=q,d|=M;if(m=m.next,m===null){if(m=u.shared.pending,m===null)break;M=m,m=M.next,M.next=null,u.lastBaseUpdate=M,u.shared.pending=null}}while(!0);if(z===null&&(y=U),u.baseState=y,u.firstBaseUpdate=P,u.lastBaseUpdate=z,t=u.shared.interleaved,t!==null){u=t;do d|=u.lane,u=u.next;while(u!==t)}else a===null&&(u.shared.lanes=0);On|=d,e.lanes=d,e.memoizedState=U}}function Mc(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var o=_l.transition;_l.transition={};try{e(!1),t()}finally{xe=n,_l.transition=o}}function tf(){return kt().memoizedState}function Rm(e,t,n){var o=xn(e);if(n={lane:o,action:n,hasEagerState:!1,eagerState:null,next:null},nf(e))rf(t,n);else if(n=Oc(e,t,n,o),n!==null){var u=tt();Dt(n,e,o,u),of(n,t,o)}}function Pm(e,t,n){var o=xn(e),u={lane:o,action:n,hasEagerState:!1,eagerState:null,next:null};if(nf(e))rf(t,u);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var d=t.lastRenderedState,m=a(d,n);if(u.hasEagerState=!0,u.eagerState=m,Tt(m,d)){var y=t.interleaved;y===null?(u.next=u,Cl(t)):(u.next=y.next,y.next=u),t.interleaved=u;return}}catch{}finally{}n=Oc(e,t,u,o),n!==null&&(u=tt(),Dt(n,e,o,u),of(n,t,o))}}function nf(e){var t=e.alternate;return e===Pe||t!==null&&t===Pe}function rf(e,t){so=gi=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function of(e,t,n){if(n&4194240){var o=t.lanes;o&=e.pendingLanes,n|=o,t.lanes=n,Us(e,n)}}var wi={readContext:Ct,useCallback:qe,useContext:qe,useEffect:qe,useImperativeHandle:qe,useInsertionEffect:qe,useLayoutEffect:qe,useMemo:qe,useReducer:qe,useRef:qe,useState:qe,useDebugValue:qe,useDeferredValue:qe,useTransition:qe,useMutableSource:qe,useSyncExternalStore:qe,useId:qe,unstable_isNewReconciler:!1},_m={readContext:Ct,useCallback:function(e,t){return Ht().memoizedState=[e,t===void 0?null:t],e},useContext:Ct,useEffect:qc,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,yi(4194308,4,Kc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return yi(4194308,4,e,t)},useInsertionEffect:function(e,t){return yi(4,2,e,t)},useMemo:function(e,t){var n=Ht();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var o=Ht();return t=n!==void 0?n(t):t,o.memoizedState=o.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},o.queue=e,e=e.dispatch=Rm.bind(null,Pe,e),[o.memoizedState,e]},useRef:function(e){var t=Ht();return e={current:e},t.memoizedState=e},useState:Wc,useDebugValue:Ml,useDeferredValue:function(e){return Ht().memoizedState=e},useTransition:function(){var e=Wc(!1),t=e[0];return e=Am.bind(null,e[1]),Ht().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var o=Pe,u=Ht();if(Ae){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),Fe===null)throw Error(s(349));Nn&30||Bc(o,t,n)}u.memoizedState=n;var a={value:n,getSnapshot:t};return u.queue=a,qc(Hc.bind(null,o,a,e),[e]),o.flags|=2048,ao(9,$c.bind(null,o,a,n,t),void 0,null),n},useId:function(){var e=Ht(),t=Fe.identifierPrefix;if(Ae){var n=Kt,o=Gt;n=(o&~(1<<32-_t(o)-1)).toString(32)+n,t=":"+t+"R"+n,n=lo++,0<\/script>",e=e.removeChild(e.firstChild)):typeof o.is=="string"?e=d.createElement(n,{is:o.is}):(e=d.createElement(n),n==="select"&&(d=e,o.multiple?d.multiple=!0:o.size&&(d.size=o.size))):e=d.createElementNS(e,n),e[Bt]=t,e[eo]=o,jf(e,t,!1,!1),t.stateNode=e;e:{switch(d=Ps(n,o),n){case"dialog":Ce("cancel",e),Ce("close",e),u=o;break;case"iframe":case"object":case"embed":Ce("load",e),u=o;break;case"video":case"audio":for(u=0;ugr&&(t.flags|=128,o=!0,co(a,!1),t.lanes=4194304)}else{if(!o)if(e=hi(d),e!==null){if(t.flags|=128,o=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),co(a,!0),a.tail===null&&a.tailMode==="hidden"&&!d.alternate&&!Ae)return Qe(t),null}else 2*Te()-a.renderingStartTime>gr&&n!==1073741824&&(t.flags|=128,o=!0,co(a,!1),t.lanes=4194304);a.isBackwards?(d.sibling=t.child,t.child=d):(n=a.last,n!==null?n.sibling=d:t.child=d,a.last=d)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=Te(),t.sibling=null,n=Re.current,Ee(Re,o?n&1|2:n&1),t):(Qe(t),null);case 22:case 23:return su(),o=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==o&&(t.flags|=8192),o&&t.mode&1?ht&1073741824&&(Qe(t),t.subtreeFlags&6&&(t.flags|=8192)):Qe(t),null;case 24:return null;case 25:return null}throw Error(s(156,t.tag))}function zm(e,t){switch(ml(t),t.tag){case 1:return rt(t.type)&&ri(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return dr(),ke(nt),ke(Ye),Pl(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Al(t),null;case 13:if(ke(Re),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));ur()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ke(Re),null;case 4:return dr(),null;case 10:return Sl(t.type._context),null;case 22:case 23:return su(),null;case 24:return null;default:return null}}var Ci=!1,Ge=!1,Um=typeof WeakSet=="function"?WeakSet:Set,G=null;function hr(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(o){_e(e,t,o)}else n.current=null}function Ql(e,t,n){try{n()}catch(o){_e(e,t,o)}}var Pf=!1;function Fm(e,t){if(sl=bo,e=ic(),Js(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var o=n.getSelection&&n.getSelection();if(o&&o.rangeCount!==0){n=o.anchorNode;var u=o.anchorOffset,a=o.focusNode;o=o.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break e}var d=0,m=-1,y=-1,P=0,z=0,U=e,M=null;t:for(;;){for(var q;U!==n||u!==0&&U.nodeType!==3||(m=d+u),U!==a||o!==0&&U.nodeType!==3||(y=d+o),U.nodeType===3&&(d+=U.nodeValue.length),(q=U.firstChild)!==null;)M=U,U=q;for(;;){if(U===e)break t;if(M===n&&++P===u&&(m=d),M===a&&++z===o&&(y=d),(q=U.nextSibling)!==null)break;U=M,M=U.parentNode}U=q}n=m===-1||y===-1?null:{start:m,end:y}}else n=null}n=n||{start:0,end:0}}else n=null;for(ll={focusedElem:e,selectionRange:n},bo=!1,G=t;G!==null;)if(t=G,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,G=e;else for(;G!==null;){t=G;try{var K=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(K!==null){var X=K.memoizedProps,Ie=K.memoizedState,k=t.stateNode,w=k.getSnapshotBeforeUpdate(t.elementType===t.type?X:Nt(t.type,X),Ie);k.__reactInternalSnapshotBeforeUpdate=w}break;case 3:var j=t.stateNode.containerInfo;j.nodeType===1?j.textContent="":j.nodeType===9&&j.documentElement&&j.removeChild(j.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(s(163))}}catch($){_e(t,t.return,$)}if(e=t.sibling,e!==null){e.return=t.return,G=e;break}G=t.return}return K=Pf,Pf=!1,K}function fo(e,t,n){var o=t.updateQueue;if(o=o!==null?o.lastEffect:null,o!==null){var u=o=o.next;do{if((u.tag&e)===e){var a=u.destroy;u.destroy=void 0,a!==void 0&&Ql(t,n,a)}u=u.next}while(u!==o)}}function ki(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var o=n.create;n.destroy=o()}n=n.next}while(n!==t)}}function Gl(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function _f(e){var t=e.alternate;t!==null&&(e.alternate=null,_f(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Bt],delete t[eo],delete t[fl],delete t[Sm],delete t[Em])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Tf(e){return e.tag===5||e.tag===3||e.tag===4}function If(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Tf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Kl(e,t,n){var o=e.tag;if(o===5||o===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ti));else if(o!==4&&(e=e.child,e!==null))for(Kl(e,t,n),e=e.sibling;e!==null;)Kl(e,t,n),e=e.sibling}function Xl(e,t,n){var o=e.tag;if(o===5||o===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(o!==4&&(e=e.child,e!==null))for(Xl(e,t,n),e=e.sibling;e!==null;)Xl(e,t,n),e=e.sibling}var be=null,Ot=!1;function yn(e,t,n){for(n=n.child;n!==null;)Nf(e,t,n),n=n.sibling}function Nf(e,t,n){if(Ft&&typeof Ft.onCommitFiberUnmount=="function")try{Ft.onCommitFiberUnmount(zo,n)}catch{}switch(n.tag){case 5:Ge||hr(n,t);case 6:var o=be,u=Ot;be=null,yn(e,t,n),be=o,Ot=u,be!==null&&(Ot?(e=be,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):be.removeChild(n.stateNode));break;case 18:be!==null&&(Ot?(e=be,n=n.stateNode,e.nodeType===8?cl(e.parentNode,n):e.nodeType===1&&cl(e,n),br(e)):cl(be,n.stateNode));break;case 4:o=be,u=Ot,be=n.stateNode.containerInfo,Ot=!0,yn(e,t,n),be=o,Ot=u;break;case 0:case 11:case 14:case 15:if(!Ge&&(o=n.updateQueue,o!==null&&(o=o.lastEffect,o!==null))){u=o=o.next;do{var a=u,d=a.destroy;a=a.tag,d!==void 0&&(a&2||a&4)&&Ql(n,t,d),u=u.next}while(u!==o)}yn(e,t,n);break;case 1:if(!Ge&&(hr(n,t),o=n.stateNode,typeof o.componentWillUnmount=="function"))try{o.props=n.memoizedProps,o.state=n.memoizedState,o.componentWillUnmount()}catch(m){_e(n,t,m)}yn(e,t,n);break;case 21:yn(e,t,n);break;case 22:n.mode&1?(Ge=(o=Ge)||n.memoizedState!==null,yn(e,t,n),Ge=o):yn(e,t,n);break;default:yn(e,t,n)}}function Of(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Um),t.forEach(function(o){var u=Qm.bind(null,e,o);n.has(o)||(n.add(o),o.then(u,u))})}}function Lt(e,t){var n=t.deletions;if(n!==null)for(var o=0;ou&&(u=d),o&=~a}if(o=u,o=Te()-o,o=(120>o?120:480>o?480:1080>o?1080:1920>o?1920:3e3>o?3e3:4320>o?4320:1960*$m(o/1960))-o,10e?16:e,wn===null)var o=!1;else{if(e=wn,wn=null,_i=0,he&6)throw Error(s(331));var u=he;for(he|=4,G=e.current;G!==null;){var a=G,d=a.child;if(G.flags&16){var m=a.deletions;if(m!==null){for(var y=0;yTe()-eu?Dn(e,0):Zl|=n),st(e,t)}function Yf(e,t){t===0&&(e.mode&1?(t=Fo,Fo<<=1,!(Fo&130023424)&&(Fo=4194304)):t=1);var n=tt();e=Xt(e,t),e!==null&&(Ur(e,t,n),st(e,n))}function qm(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Yf(e,n)}function Qm(e,t){var n=0;switch(e.tag){case 13:var o=e.stateNode,u=e.memoizedState;u!==null&&(n=u.retryLane);break;case 19:o=e.stateNode;break;default:throw Error(s(314))}o!==null&&o.delete(t),Yf(e,n)}var qf;qf=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||nt.current)ot=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ot=!1,Dm(e,t,n);ot=!!(e.flags&131072)}else ot=!1,Ae&&t.flags&1048576&&jc(t,li,t.index);switch(t.lanes=0,t.tag){case 2:var o=t.type;Ei(e,t),e=t.pendingProps;var u=ir(t,Ye.current);fr(t,n),u=Il(null,t,o,e,u,n);var a=Nl();return t.flags|=1,typeof u=="object"&&u!==null&&typeof u.render=="function"&&u.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,rt(o)?(a=!0,oi(t)):a=!1,t.memoizedState=u.state!==null&&u.state!==void 0?u.state:null,kl(t),u.updater=xi,t.stateNode=u,u._reactInternals=t,Ul(t,o,e,n),t=Hl(null,t,o,!0,a,n)):(t.tag=0,Ae&&a&&hl(t),et(null,t,u,n),t=t.child),t;case 16:o=t.elementType;e:{switch(Ei(e,t),e=t.pendingProps,u=o._init,o=u(o._payload),t.type=o,u=t.tag=Km(o),e=Nt(o,e),u){case 0:t=$l(null,t,o,e,n);break e;case 1:t=wf(null,t,o,e,n);break e;case 11:t=hf(null,t,o,e,n);break e;case 14:t=mf(null,t,o,Nt(o.type,e),n);break e}throw Error(s(306,o,""))}return t;case 0:return o=t.type,u=t.pendingProps,u=t.elementType===o?u:Nt(o,u),$l(e,t,o,u,n);case 1:return o=t.type,u=t.pendingProps,u=t.elementType===o?u:Nt(o,u),wf(e,t,o,u,n);case 3:e:{if(xf(t),e===null)throw Error(s(387));o=t.pendingProps,a=t.memoizedState,u=a.element,Lc(e,t),pi(t,o,null,n);var d=t.memoizedState;if(o=d.element,a.isDehydrated)if(a={element:o,isDehydrated:!1,cache:d.cache,pendingSuspenseBoundaries:d.pendingSuspenseBoundaries,transitions:d.transitions},t.updateQueue.baseState=a,t.memoizedState=a,t.flags&256){u=pr(Error(s(423)),t),t=Sf(e,t,o,n,u);break e}else if(o!==u){u=pr(Error(s(424)),t),t=Sf(e,t,o,n,u);break e}else for(pt=fn(t.stateNode.containerInfo.firstChild),dt=t,Ae=!0,It=null,n=Nc(t,null,o,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(ur(),o===u){t=Zt(e,t,n);break e}et(e,t,o,n)}t=t.child}return t;case 5:return zc(t),e===null&&yl(t),o=t.type,u=t.pendingProps,a=e!==null?e.memoizedProps:null,d=u.children,ul(o,u)?d=null:a!==null&&ul(o,a)&&(t.flags|=32),vf(e,t),et(e,t,d,n),t.child;case 6:return e===null&&yl(t),null;case 13:return Ef(e,t,n);case 4:return jl(t,t.stateNode.containerInfo),o=t.pendingProps,e===null?t.child=ar(t,null,o,n):et(e,t,o,n),t.child;case 11:return o=t.type,u=t.pendingProps,u=t.elementType===o?u:Nt(o,u),hf(e,t,o,u,n);case 7:return et(e,t,t.pendingProps,n),t.child;case 8:return et(e,t,t.pendingProps.children,n),t.child;case 12:return et(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(o=t.type._context,u=t.pendingProps,a=t.memoizedProps,d=u.value,Ee(ci,o._currentValue),o._currentValue=d,a!==null)if(Tt(a.value,d)){if(a.children===u.children&&!nt.current){t=Zt(e,t,n);break e}}else for(a=t.child,a!==null&&(a.return=t);a!==null;){var m=a.dependencies;if(m!==null){d=a.child;for(var y=m.firstContext;y!==null;){if(y.context===o){if(a.tag===1){y=Jt(-1,n&-n),y.tag=2;var P=a.updateQueue;if(P!==null){P=P.shared;var z=P.pending;z===null?y.next=y:(y.next=z.next,z.next=y),P.pending=y}}a.lanes|=n,y=a.alternate,y!==null&&(y.lanes|=n),El(a.return,n,t),m.lanes|=n;break}y=y.next}}else if(a.tag===10)d=a.type===t.type?null:a.child;else if(a.tag===18){if(d=a.return,d===null)throw Error(s(341));d.lanes|=n,m=d.alternate,m!==null&&(m.lanes|=n),El(d,n,t),d=a.sibling}else d=a.child;if(d!==null)d.return=a;else for(d=a;d!==null;){if(d===t){d=null;break}if(a=d.sibling,a!==null){a.return=d.return,d=a;break}d=d.return}a=d}et(e,t,u.children,n),t=t.child}return t;case 9:return u=t.type,o=t.pendingProps.children,fr(t,n),u=Ct(u),o=o(u),t.flags|=1,et(e,t,o,n),t.child;case 14:return o=t.type,u=Nt(o,t.pendingProps),u=Nt(o.type,u),mf(e,t,o,u,n);case 15:return gf(e,t,t.type,t.pendingProps,n);case 17:return o=t.type,u=t.pendingProps,u=t.elementType===o?u:Nt(o,u),Ei(e,t),t.tag=1,rt(o)?(e=!0,oi(t)):e=!1,fr(t,n),lf(t,o,u),Ul(t,o,u,n),Hl(null,t,o,!0,e,n);case 19:return kf(e,t,n);case 22:return yf(e,t,n)}throw Error(s(156,t.tag))};function Qf(e,t){return Aa(e,t)}function Gm(e,t,n,o){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=o,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function At(e,t,n,o){return new Gm(e,t,n,o)}function uu(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Km(e){if(typeof e=="function")return uu(e)?1:0;if(e!=null){if(e=e.$$typeof,e===wt)return 11;if(e===xt)return 14}return 2}function En(e,t){var n=e.alternate;return n===null?(n=At(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Oi(e,t,n,o,u,a){var d=2;if(o=e,typeof e=="function")uu(e)&&(d=1);else if(typeof e=="string")d=5;else e:switch(e){case b:return zn(n.children,u,a,t);case re:d=8,u|=8;break;case ye:return e=At(12,n,t,u|2),e.elementType=ye,e.lanes=a,e;case Ze:return e=At(13,n,t,u),e.elementType=Ze,e.lanes=a,e;case ct:return e=At(19,n,t,u),e.elementType=ct,e.lanes=a,e;case Se:return Li(n,u,a,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ne:d=10;break e;case at:d=9;break e;case wt:d=11;break e;case xt:d=14;break e;case We:d=16,o=null;break e}throw Error(s(130,e==null?e:typeof e,""))}return t=At(d,n,t,u),t.elementType=e,t.type=o,t.lanes=a,t}function zn(e,t,n,o){return e=At(7,e,o,t),e.lanes=n,e}function Li(e,t,n,o){return e=At(22,e,o,t),e.elementType=Se,e.lanes=n,e.stateNode={isHidden:!1},e}function au(e,t,n){return e=At(6,e,null,t),e.lanes=n,e}function cu(e,t,n){return t=At(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Xm(e,t,n,o,u){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=zs(0),this.expirationTimes=zs(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=zs(0),this.identifierPrefix=o,this.onRecoverableError=u,this.mutableSourceEagerHydrationData=null}function fu(e,t,n,o,u,a,d,m,y){return e=new Xm(e,t,n,m,y),t===1?(t=1,a===!0&&(t|=8)):t=0,a=At(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:o,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},kl(a),e}function Jm(e,t,n){var o=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(i){console.error(i)}}return r(),yu.exports=fg(),yu.exports}var ad;function pg(){if(ad)return $i;ad=1;var r=dg();return $i.createRoot=r.createRoot,$i.hydrateRoot=r.hydrateRoot,$i}var hg=pg(),Xe=function(){return Xe=Object.assign||function(i){for(var s,l=1,c=arguments.length;l0?$e(Ar,--Rt):0,Cr--,Le===10&&(Cr=1,fs--),Le}function Mt(){return Le=Rt2||Lu(Le)>3?"":" "}function kg(r,i){for(;--i&&Mt()&&!(Le<48||Le>102||Le>57&&Le<65||Le>70&&Le<97););return ps(r,Gi()+(i<6&&Bn()==32&&Mt()==32))}function Du(r){for(;Mt();)switch(Le){case r:return Rt;case 34:case 39:r!==34&&r!==39&&Du(Le);break;case 40:r===41&&Du(r);break;case 92:Mt();break}return Rt}function jg(r,i){for(;Mt()&&r+Le!==57;)if(r+Le===84&&Bn()===47)break;return"/*"+ps(i,Rt-1)+"*"+Ju(r===47?r:Mt())}function Ag(r){for(;!Lu(Bn());)Mt();return ps(r,Rt)}function Rg(r){return Eg(Ki("",null,null,null,[""],r=Sg(r),0,[0],r))}function Ki(r,i,s,l,c,f,p,g,x){for(var v=0,S=0,A=p,R=0,I=0,_=0,C=1,O=1,F=1,B=0,V="",Q=c,H=f,L=l,b=V;O;)switch(_=B,B=Mt()){case 40:if(_!=108&&$e(b,A-1)==58){Qi(b+=ae(xu(B),"&","&\f"),"&\f",up(v?g[v-1]:0))!=-1&&(F=-1);break}case 34:case 39:case 91:b+=xu(B);break;case 9:case 10:case 13:case 32:b+=Cg(_);break;case 92:b+=kg(Gi()-1,7);continue;case 47:switch(Bn()){case 42:case 47:Eo(Pg(jg(Mt(),Gi()),i,s,x),x);break;default:b+="/"}break;case 123*C:g[v++]=Wt(b)*F;case 125*C:case 59:case 0:switch(B){case 0:case 125:O=0;case 59+S:F==-1&&(b=ae(b,/\f/g,"")),I>0&&Wt(b)-A&&Eo(I>32?dd(b+";",l,s,A-1,x):dd(ae(b," ","")+";",l,s,A-2,x),x);break;case 59:b+=";";default:if(Eo(L=fd(b,i,s,v,S,c,g,V,Q=[],H=[],A,f),f),B===123)if(S===0)Ki(b,i,L,L,Q,f,A,g,H);else switch(R===99&&$e(b,3)===110?100:R){case 100:case 108:case 109:case 115:Ki(r,L,L,l&&Eo(fd(r,L,L,0,0,c,g,V,c,Q=[],A,H),H),c,H,A,g,l?Q:H);break;default:Ki(b,L,L,L,[""],H,0,g,H)}}v=S=I=0,C=F=1,V=b="",A=p;break;case 58:A=1+Wt(b),I=_;default:if(C<1){if(B==123)--C;else if(B==125&&C++==0&&xg()==125)continue}switch(b+=Ju(B),B*C){case 38:F=S>0?1:(b+="\f",-1);break;case 44:g[v++]=(Wt(b)-1)*F,F=1;break;case 64:Bn()===45&&(b+=xu(Mt())),R=Bn(),S=A=Wt(V=b+=Ag(Gi())),B++;break;case 45:_===45&&Wt(b)==2&&(C=0)}}return f}function fd(r,i,s,l,c,f,p,g,x,v,S,A){for(var R=c-1,I=c===0?f:[""],_=cp(I),C=0,O=0,F=0;C0?I[B]+" "+V:ae(V,/&\f/g,I[B])))&&(x[F++]=Q);return ds(r,i,s,c===0?cs:g,x,v,S,A)}function Pg(r,i,s,l){return ds(r,i,s,sp,Ju(wg()),Er(r,2,-2),0,l)}function dd(r,i,s,l,c){return ds(r,i,s,Xu,Er(r,0,l),Er(r,l+1,-1),l,c)}function dp(r,i,s){switch(yg(r,i)){case 5103:return we+"print-"+r+r;case 5737:case 4201:case 3177:case 3433:case 1641:case 4457:case 2921:case 5572:case 6356:case 5844:case 3191:case 6645:case 3005:case 6391:case 5879:case 5623:case 6135:case 4599:case 4855:case 4215:case 6389:case 5109:case 5365:case 5621:case 3829:return we+r+r;case 4789:return Co+r+r;case 5349:case 4246:case 4810:case 6968:case 2756:return we+r+Co+r+je+r+r;case 5936:switch($e(r,i+11)){case 114:return we+r+je+ae(r,/[svh]\w+-[tblr]{2}/,"tb")+r;case 108:return we+r+je+ae(r,/[svh]\w+-[tblr]{2}/,"tb-rl")+r;case 45:return we+r+je+ae(r,/[svh]\w+-[tblr]{2}/,"lr")+r}case 6828:case 4268:case 2903:return we+r+je+r+r;case 6165:return we+r+je+"flex-"+r+r;case 5187:return we+r+ae(r,/(\w+).+(:[^]+)/,we+"box-$1$2"+je+"flex-$1$2")+r;case 5443:return we+r+je+"flex-item-"+ae(r,/flex-|-self/g,"")+(tn(r,/flex-|baseline/)?"":je+"grid-row-"+ae(r,/flex-|-self/g,""))+r;case 4675:return we+r+je+"flex-line-pack"+ae(r,/align-content|flex-|-self/g,"")+r;case 5548:return we+r+je+ae(r,"shrink","negative")+r;case 5292:return we+r+je+ae(r,"basis","preferred-size")+r;case 6060:return we+"box-"+ae(r,"-grow","")+we+r+je+ae(r,"grow","positive")+r;case 4554:return we+ae(r,/([^-])(transform)/g,"$1"+we+"$2")+r;case 6187:return ae(ae(ae(r,/(zoom-|grab)/,we+"$1"),/(image-set)/,we+"$1"),r,"")+r;case 5495:case 3959:return ae(r,/(image-set\([^]*)/,we+"$1$`$1");case 4968:return ae(ae(r,/(.+:)(flex-)?(.*)/,we+"box-pack:$3"+je+"flex-pack:$3"),/s.+-b[^;]+/,"justify")+we+r+r;case 4200:if(!tn(r,/flex-|baseline/))return je+"grid-column-align"+Er(r,i)+r;break;case 2592:case 3360:return je+ae(r,"template-","")+r;case 4384:case 3616:return s&&s.some(function(l,c){return i=c,tn(l.props,/grid-\w+-end/)})?~Qi(r+(s=s[i].value),"span",0)?r:je+ae(r,"-start","")+r+je+"grid-row-span:"+(~Qi(s,"span",0)?tn(s,/\d+/):+tn(s,/\d+/)-+tn(r,/\d+/))+";":je+ae(r,"-start","")+r;case 4896:case 4128:return s&&s.some(function(l){return tn(l.props,/grid-\w+-start/)})?r:je+ae(ae(r,"-end","-span"),"span ","")+r;case 4095:case 3583:case 4068:case 2532:return ae(r,/(.+)-inline(.+)/,we+"$1$2")+r;case 8116:case 7059:case 5753:case 5535:case 5445:case 5701:case 4933:case 4677:case 5533:case 5789:case 5021:case 4765:if(Wt(r)-1-i>6)switch($e(r,i+1)){case 109:if($e(r,i+4)!==45)break;case 102:return ae(r,/(.+:)(.+)-([^]+)/,"$1"+we+"$2-$3$1"+Co+($e(r,i+3)==108?"$3":"$2-$3"))+r;case 115:return~Qi(r,"stretch",0)?dp(ae(r,"stretch","fill-available"),i,s)+r:r}break;case 5152:case 5920:return ae(r,/(.+?):(\d+)(\s*\/\s*(span)?\s*(\d+))?(.*)/,function(l,c,f,p,g,x,v){return je+c+":"+f+v+(p?je+c+"-span:"+(g?x:+x-+f)+v:"")+r});case 4949:if($e(r,i+6)===121)return ae(r,":",":"+we)+r;break;case 6444:switch($e(r,$e(r,14)===45?18:11)){case 120:return ae(r,/(.+:)([^;\s!]+)(;|(\s+)?!.+)?/,"$1"+we+($e(r,14)===45?"inline-":"")+"box$3$1"+we+"$2$3$1"+je+"$2box$3")+r;case 100:return ae(r,":",":"+je)+r}break;case 5719:case 2647:case 2135:case 3927:case 2391:return ae(r,"scroll-","scroll-snap-")+r}return r}function rs(r,i){for(var s="",l=0;l-1&&!r.return)switch(r.type){case Xu:r.return=dp(r.value,r.length,s);return;case lp:return rs([kn(r,{value:ae(r.value,"@","@"+we)})],l);case cs:if(r.length)return vg(s=r.props,function(c){switch(tn(c,l=/(::plac\w+|:read-\w+)/)){case":read-only":case":read-write":vr(kn(r,{props:[ae(c,/:(read-\w+)/,":"+Co+"$1")]})),vr(kn(r,{props:[c]})),Ou(r,{props:cd(s,l)});break;case"::placeholder":vr(kn(r,{props:[ae(c,/:(plac\w+)/,":"+we+"input-$1")]})),vr(kn(r,{props:[ae(c,/:(plac\w+)/,":"+Co+"$1")]})),vr(kn(r,{props:[ae(c,/:(plac\w+)/,je+"input-$1")]})),vr(kn(r,{props:[c]})),Ou(r,{props:cd(s,l)});break}return""})}}var Og={animationIterationCount:1,aspectRatio:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},mt={},kr=typeof process<"u"&&mt!==void 0&&(mt.REACT_APP_SC_ATTR||mt.SC_ATTR)||"data-styled",pp="active",hp="data-styled-version",hs="6.1.14",Zu=`/*!sc*/ +`,os=typeof window<"u"&&"HTMLElement"in window,Lg=!!(typeof SC_DISABLE_SPEEDY=="boolean"?SC_DISABLE_SPEEDY:typeof process<"u"&&mt!==void 0&&mt.REACT_APP_SC_DISABLE_SPEEDY!==void 0&&mt.REACT_APP_SC_DISABLE_SPEEDY!==""?mt.REACT_APP_SC_DISABLE_SPEEDY!=="false"&&mt.REACT_APP_SC_DISABLE_SPEEDY:typeof process<"u"&&mt!==void 0&&mt.SC_DISABLE_SPEEDY!==void 0&&mt.SC_DISABLE_SPEEDY!==""&&mt.SC_DISABLE_SPEEDY!=="false"&&mt.SC_DISABLE_SPEEDY),ms=Object.freeze([]),jr=Object.freeze({});function Dg(r,i,s){return s===void 0&&(s=jr),r.theme!==s.theme&&r.theme||i||s.theme}var mp=new Set(["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","track","u","ul","use","var","video","wbr","circle","clipPath","defs","ellipse","foreignObject","g","image","line","linearGradient","marker","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","svg","text","tspan"]),Mg=/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~-]+/g,zg=/(^-|-$)/g;function pd(r){return r.replace(Mg,"-").replace(zg,"")}var Ug=/(a)(d)/gi,Hi=52,hd=function(r){return String.fromCharCode(r+(r>25?39:97))};function Mu(r){var i,s="";for(i=Math.abs(r);i>Hi;i=i/Hi|0)s=hd(i%Hi)+s;return(hd(i%Hi)+s).replace(Ug,"$1-$2")}var Su,gp=5381,wr=function(r,i){for(var s=i.length;s;)r=33*r^i.charCodeAt(--s);return r},yp=function(r){return wr(gp,r)};function Fg(r){return Mu(yp(r)>>>0)}function Bg(r){return r.displayName||r.name||"Component"}function Eu(r){return typeof r=="string"&&!0}var vp=typeof Symbol=="function"&&Symbol.for,wp=vp?Symbol.for("react.memo"):60115,$g=vp?Symbol.for("react.forward_ref"):60112,Hg={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},bg={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},xp={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},Vg=((Su={})[$g]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},Su[wp]=xp,Su);function md(r){return("type"in(i=r)&&i.type.$$typeof)===wp?xp:"$$typeof"in r?Vg[r.$$typeof]:Hg;var i}var Wg=Object.defineProperty,Yg=Object.getOwnPropertyNames,gd=Object.getOwnPropertySymbols,qg=Object.getOwnPropertyDescriptor,Qg=Object.getPrototypeOf,yd=Object.prototype;function Sp(r,i,s){if(typeof i!="string"){if(yd){var l=Qg(i);l&&l!==yd&&Sp(r,l,s)}var c=Yg(i);gd&&(c=c.concat(gd(i)));for(var f=md(r),p=md(i),g=0;g0?" Args: ".concat(i.join(", ")):""))}var Gg=function(){function r(i){this.groupSizes=new Uint32Array(512),this.length=512,this.tag=i}return r.prototype.indexOfGroup=function(i){for(var s=0,l=0;l=this.groupSizes.length){for(var l=this.groupSizes,c=l.length,f=c;i>=f;)if((f<<=1)<0)throw Vn(16,"".concat(i));this.groupSizes=new Uint32Array(f),this.groupSizes.set(l),this.length=f;for(var p=c;p=this.length||this.groupSizes[i]===0)return s;for(var l=this.groupSizes[i],c=this.indexOfGroup(i),f=c+l,p=c;p=0){var l=document.createTextNode(s);return this.element.insertBefore(l,this.nodes[i]||null),this.length++,!0}return!1},r.prototype.deleteRule=function(i){this.element.removeChild(this.nodes[i]),this.length--},r.prototype.getRule=function(i){return i0&&(O+="".concat(F,","))}),x+="".concat(_).concat(C,'{content:"').concat(O,'"}').concat(Zu)},S=0;S0?".".concat(i):R},S=x.slice();S.push(function(R){R.type===cs&&R.value.includes("&")&&(R.props[0]=R.props[0].replace(sy,s).replace(l,v))}),p.prefix&&S.push(Ng),S.push(_g);var A=function(R,I,_,C){I===void 0&&(I=""),_===void 0&&(_=""),C===void 0&&(C="&"),i=C,s=I,l=new RegExp("\\".concat(s,"\\b"),"g");var O=R.replace(ly,""),F=Rg(_||I?"".concat(_," ").concat(I," { ").concat(O," }"):O);p.namespace&&(F=kp(F,p.namespace));var B=[];return rs(F,Tg(S.concat(Ig(function(V){return B.push(V)})))),B};return A.hash=x.length?x.reduce(function(R,I){return I.name||Vn(15),wr(R,I.name)},gp).toString():"",A}var ay=new Cp,Uu=uy(),jp=gt.createContext({shouldForwardProp:void 0,styleSheet:ay,stylis:Uu});jp.Consumer;gt.createContext(void 0);function Sd(){return ie.useContext(jp)}var cy=function(){function r(i,s){var l=this;this.inject=function(c,f){f===void 0&&(f=Uu);var p=l.name+f.hash;c.hasNameForId(l.id,p)||c.insertRules(l.id,p,f(l.rules,p,"@keyframes"))},this.name=i,this.id="sc-keyframes-".concat(i),this.rules=s,ta(this,function(){throw Vn(12,String(l.name))})}return r.prototype.getName=function(i){return i===void 0&&(i=Uu),this.name+i.hash},r}(),fy=function(r){return r>="A"&&r<="Z"};function Ed(r){for(var i="",s=0;s>>0);if(!s.hasNameForId(this.componentId,p)){var g=l(f,".".concat(p),void 0,this.componentId);s.insertRules(this.componentId,p,g)}c=Un(c,p),this.staticRulesId=p}else{for(var x=wr(this.baseHash,l.hash),v="",S=0;S>>0);s.hasNameForId(this.componentId,I)||s.insertRules(this.componentId,I,l(v,".".concat(I),void 0,this.componentId)),c=Un(c,I)}}return c},r}(),ss=gt.createContext(void 0);ss.Consumer;function Cd(r){var i=gt.useContext(ss),s=ie.useMemo(function(){return function(l,c){if(!l)throw Vn(14);if(bn(l)){var f=l(c);return f}if(Array.isArray(l)||typeof l!="object")throw Vn(8);return c?Xe(Xe({},c),l):l}(r.theme,i)},[r.theme,i]);return r.children?gt.createElement(ss.Provider,{value:s},r.children):null}var Cu={};function my(r,i,s){var l=ea(r),c=r,f=!Eu(r),p=i.attrs,g=p===void 0?ms:p,x=i.componentId,v=x===void 0?function(Q,H){var L=typeof Q!="string"?"sc":pd(Q);Cu[L]=(Cu[L]||0)+1;var b="".concat(L,"-").concat(Fg(hs+L+Cu[L]));return H?"".concat(H,"-").concat(b):b}(i.displayName,i.parentComponentId):x,S=i.displayName,A=S===void 0?function(Q){return Eu(Q)?"styled.".concat(Q):"Styled(".concat(Bg(Q),")")}(r):S,R=i.displayName&&i.componentId?"".concat(pd(i.displayName),"-").concat(i.componentId):i.componentId||v,I=l&&c.attrs?c.attrs.concat(g).filter(Boolean):g,_=i.shouldForwardProp;if(l&&c.shouldForwardProp){var C=c.shouldForwardProp;if(i.shouldForwardProp){var O=i.shouldForwardProp;_=function(Q,H){return C(Q,H)&&O(Q,H)}}else _=C}var F=new hy(s,R,l?c.componentStyle:void 0);function B(Q,H){return function(L,b,re){var ye=L.attrs,Ne=L.componentStyle,at=L.defaultProps,wt=L.foldedComponentIds,Ze=L.styledComponentId,ct=L.target,xt=gt.useContext(ss),We=Sd(),Se=L.shouldForwardProp||We.shouldForwardProp,W=Dg(b,xt,at)||jr,Z=function(de,ce,ve){for(var pe,me=Xe(Xe({},ce),{className:void 0,theme:ve}),He=0;Hei=>{const s=yy.call(i);return r[s]||(r[s]=s.slice(8,-1).toLowerCase())})(Object.create(null)),Ut=r=>(r=r.toLowerCase(),i=>gs(i)===r),ys=r=>i=>typeof i===r,{isArray:Rr}=Array,Po=ys("undefined");function vy(r){return r!==null&&!Po(r)&&r.constructor!==null&&!Po(r.constructor)&&yt(r.constructor.isBuffer)&&r.constructor.isBuffer(r)}const Tp=Ut("ArrayBuffer");function wy(r){let i;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?i=ArrayBuffer.isView(r):i=r&&r.buffer&&Tp(r.buffer),i}const xy=ys("string"),yt=ys("function"),Ip=ys("number"),vs=r=>r!==null&&typeof r=="object",Sy=r=>r===!0||r===!1,Zi=r=>{if(gs(r)!=="object")return!1;const i=na(r);return(i===null||i===Object.prototype||Object.getPrototypeOf(i)===null)&&!(Symbol.toStringTag in r)&&!(Symbol.iterator in r)},Ey=Ut("Date"),Cy=Ut("File"),ky=Ut("Blob"),jy=Ut("FileList"),Ay=r=>vs(r)&&yt(r.pipe),Ry=r=>{let i;return r&&(typeof FormData=="function"&&r instanceof FormData||yt(r.append)&&((i=gs(r))==="formdata"||i==="object"&&yt(r.toString)&&r.toString()==="[object FormData]"))},Py=Ut("URLSearchParams"),[_y,Ty,Iy,Ny]=["ReadableStream","Request","Response","Headers"].map(Ut),Oy=r=>r.trim?r.trim():r.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function _o(r,i,{allOwnKeys:s=!1}={}){if(r===null||typeof r>"u")return;let l,c;if(typeof r!="object"&&(r=[r]),Rr(r))for(l=0,c=r.length;l0;)if(c=s[l],i===c.toLowerCase())return c;return null}const Fn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,Op=r=>!Po(r)&&r!==Fn;function Bu(){const{caseless:r}=Op(this)&&this||{},i={},s=(l,c)=>{const f=r&&Np(i,c)||c;Zi(i[f])&&Zi(l)?i[f]=Bu(i[f],l):Zi(l)?i[f]=Bu({},l):Rr(l)?i[f]=l.slice():i[f]=l};for(let l=0,c=arguments.length;l(_o(i,(c,f)=>{s&&yt(c)?r[f]=_p(c,s):r[f]=c},{allOwnKeys:l}),r),Dy=r=>(r.charCodeAt(0)===65279&&(r=r.slice(1)),r),My=(r,i,s,l)=>{r.prototype=Object.create(i.prototype,l),r.prototype.constructor=r,Object.defineProperty(r,"super",{value:i.prototype}),s&&Object.assign(r.prototype,s)},zy=(r,i,s,l)=>{let c,f,p;const g={};if(i=i||{},r==null)return i;do{for(c=Object.getOwnPropertyNames(r),f=c.length;f-- >0;)p=c[f],(!l||l(p,r,i))&&!g[p]&&(i[p]=r[p],g[p]=!0);r=s!==!1&&na(r)}while(r&&(!s||s(r,i))&&r!==Object.prototype);return i},Uy=(r,i,s)=>{r=String(r),(s===void 0||s>r.length)&&(s=r.length),s-=i.length;const l=r.indexOf(i,s);return l!==-1&&l===s},Fy=r=>{if(!r)return null;if(Rr(r))return r;let i=r.length;if(!Ip(i))return null;const s=new Array(i);for(;i-- >0;)s[i]=r[i];return s},By=(r=>i=>r&&i instanceof r)(typeof Uint8Array<"u"&&na(Uint8Array)),$y=(r,i)=>{const l=(r&&r[Symbol.iterator]).call(r);let c;for(;(c=l.next())&&!c.done;){const f=c.value;i.call(r,f[0],f[1])}},Hy=(r,i)=>{let s;const l=[];for(;(s=r.exec(i))!==null;)l.push(s);return l},by=Ut("HTMLFormElement"),Vy=r=>r.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(s,l,c){return l.toUpperCase()+c}),Ad=(({hasOwnProperty:r})=>(i,s)=>r.call(i,s))(Object.prototype),Wy=Ut("RegExp"),Lp=(r,i)=>{const s=Object.getOwnPropertyDescriptors(r),l={};_o(s,(c,f)=>{let p;(p=i(c,f,r))!==!1&&(l[f]=p||c)}),Object.defineProperties(r,l)},Yy=r=>{Lp(r,(i,s)=>{if(yt(r)&&["arguments","caller","callee"].indexOf(s)!==-1)return!1;const l=r[s];if(yt(l)){if(i.enumerable=!1,"writable"in i){i.writable=!1;return}i.set||(i.set=()=>{throw Error("Can not rewrite read-only method '"+s+"'")})}})},qy=(r,i)=>{const s={},l=c=>{c.forEach(f=>{s[f]=!0})};return Rr(r)?l(r):l(String(r).split(i)),s},Qy=()=>{},Gy=(r,i)=>r!=null&&Number.isFinite(r=+r)?r:i,ku="abcdefghijklmnopqrstuvwxyz",Rd="0123456789",Dp={DIGIT:Rd,ALPHA:ku,ALPHA_DIGIT:ku+ku.toUpperCase()+Rd},Ky=(r=16,i=Dp.ALPHA_DIGIT)=>{let s="";const{length:l}=i;for(;r--;)s+=i[Math.random()*l|0];return s};function Xy(r){return!!(r&&yt(r.append)&&r[Symbol.toStringTag]==="FormData"&&r[Symbol.iterator])}const Jy=r=>{const i=new Array(10),s=(l,c)=>{if(vs(l)){if(i.indexOf(l)>=0)return;if(!("toJSON"in l)){i[c]=l;const f=Rr(l)?[]:{};return _o(l,(p,g)=>{const x=s(p,c+1);!Po(x)&&(f[g]=x)}),i[c]=void 0,f}}return l};return s(r,0)},Zy=Ut("AsyncFunction"),ev=r=>r&&(vs(r)||yt(r))&&yt(r.then)&&yt(r.catch),Mp=((r,i)=>r?setImmediate:i?((s,l)=>(Fn.addEventListener("message",({source:c,data:f})=>{c===Fn&&f===s&&l.length&&l.shift()()},!1),c=>{l.push(c),Fn.postMessage(s,"*")}))(`axios@${Math.random()}`,[]):s=>setTimeout(s))(typeof setImmediate=="function",yt(Fn.postMessage)),tv=typeof queueMicrotask<"u"?queueMicrotask.bind(Fn):typeof process<"u"&&process.nextTick||Mp,N={isArray:Rr,isArrayBuffer:Tp,isBuffer:vy,isFormData:Ry,isArrayBufferView:wy,isString:xy,isNumber:Ip,isBoolean:Sy,isObject:vs,isPlainObject:Zi,isReadableStream:_y,isRequest:Ty,isResponse:Iy,isHeaders:Ny,isUndefined:Po,isDate:Ey,isFile:Cy,isBlob:ky,isRegExp:Wy,isFunction:yt,isStream:Ay,isURLSearchParams:Py,isTypedArray:By,isFileList:jy,forEach:_o,merge:Bu,extend:Ly,trim:Oy,stripBOM:Dy,inherits:My,toFlatObject:zy,kindOf:gs,kindOfTest:Ut,endsWith:Uy,toArray:Fy,forEachEntry:$y,matchAll:Hy,isHTMLForm:by,hasOwnProperty:Ad,hasOwnProp:Ad,reduceDescriptors:Lp,freezeMethods:Yy,toObjectSet:qy,toCamelCase:Vy,noop:Qy,toFiniteNumber:Gy,findKey:Np,global:Fn,isContextDefined:Op,ALPHABET:Dp,generateString:Ky,isSpecCompliantForm:Xy,toJSONObject:Jy,isAsyncFn:Zy,isThenable:ev,setImmediate:Mp,asap:tv};function le(r,i,s,l,c){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack,this.message=r,this.name="AxiosError",i&&(this.code=i),s&&(this.config=s),l&&(this.request=l),c&&(this.response=c,this.status=c.status?c.status:null)}N.inherits(le,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:N.toJSONObject(this.config),code:this.code,status:this.status}}});const zp=le.prototype,Up={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach(r=>{Up[r]={value:r}});Object.defineProperties(le,Up);Object.defineProperty(zp,"isAxiosError",{value:!0});le.from=(r,i,s,l,c,f)=>{const p=Object.create(zp);return N.toFlatObject(r,p,function(x){return x!==Error.prototype},g=>g!=="isAxiosError"),le.call(p,r.message,i,s,l,c),p.cause=r,p.name=r.name,f&&Object.assign(p,f),p};const nv=null;function $u(r){return N.isPlainObject(r)||N.isArray(r)}function Fp(r){return N.endsWith(r,"[]")?r.slice(0,-2):r}function Pd(r,i,s){return r?r.concat(i).map(function(c,f){return c=Fp(c),!s&&f?"["+c+"]":c}).join(s?".":""):i}function rv(r){return N.isArray(r)&&!r.some($u)}const ov=N.toFlatObject(N,{},null,function(i){return/^is[A-Z]/.test(i)});function ws(r,i,s){if(!N.isObject(r))throw new TypeError("target must be an object");i=i||new FormData,s=N.toFlatObject(s,{metaTokens:!0,dots:!1,indexes:!1},!1,function(C,O){return!N.isUndefined(O[C])});const l=s.metaTokens,c=s.visitor||S,f=s.dots,p=s.indexes,x=(s.Blob||typeof Blob<"u"&&Blob)&&N.isSpecCompliantForm(i);if(!N.isFunction(c))throw new TypeError("visitor must be a function");function v(_){if(_===null)return"";if(N.isDate(_))return _.toISOString();if(!x&&N.isBlob(_))throw new le("Blob is not supported. Use a Buffer instead.");return N.isArrayBuffer(_)||N.isTypedArray(_)?x&&typeof Blob=="function"?new Blob([_]):Buffer.from(_):_}function S(_,C,O){let F=_;if(_&&!O&&typeof _=="object"){if(N.endsWith(C,"{}"))C=l?C:C.slice(0,-2),_=JSON.stringify(_);else if(N.isArray(_)&&rv(_)||(N.isFileList(_)||N.endsWith(C,"[]"))&&(F=N.toArray(_)))return C=Fp(C),F.forEach(function(V,Q){!(N.isUndefined(V)||V===null)&&i.append(p===!0?Pd([C],Q,f):p===null?C:C+"[]",v(V))}),!1}return $u(_)?!0:(i.append(Pd(O,C,f),v(_)),!1)}const A=[],R=Object.assign(ov,{defaultVisitor:S,convertValue:v,isVisitable:$u});function I(_,C){if(!N.isUndefined(_)){if(A.indexOf(_)!==-1)throw Error("Circular reference detected in "+C.join("."));A.push(_),N.forEach(_,function(F,B){(!(N.isUndefined(F)||F===null)&&c.call(i,F,N.isString(B)?B.trim():B,C,R))===!0&&I(F,C?C.concat(B):[B])}),A.pop()}}if(!N.isObject(r))throw new TypeError("data must be an object");return I(r),i}function _d(r){const i={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(r).replace(/[!'()~]|%20|%00/g,function(l){return i[l]})}function ra(r,i){this._pairs=[],r&&ws(r,this,i)}const Bp=ra.prototype;Bp.append=function(i,s){this._pairs.push([i,s])};Bp.toString=function(i){const s=i?function(l){return i.call(this,l,_d)}:_d;return this._pairs.map(function(c){return s(c[0])+"="+s(c[1])},"").join("&")};function iv(r){return encodeURIComponent(r).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function $p(r,i,s){if(!i)return r;const l=s&&s.encode||iv;N.isFunction(s)&&(s={serialize:s});const c=s&&s.serialize;let f;if(c?f=c(i,s):f=N.isURLSearchParams(i)?i.toString():new ra(i,s).toString(l),f){const p=r.indexOf("#");p!==-1&&(r=r.slice(0,p)),r+=(r.indexOf("?")===-1?"?":"&")+f}return r}class Td{constructor(){this.handlers=[]}use(i,s,l){return this.handlers.push({fulfilled:i,rejected:s,synchronous:l?l.synchronous:!1,runWhen:l?l.runWhen:null}),this.handlers.length-1}eject(i){this.handlers[i]&&(this.handlers[i]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(i){N.forEach(this.handlers,function(l){l!==null&&i(l)})}}const Hp={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},sv=typeof URLSearchParams<"u"?URLSearchParams:ra,lv=typeof FormData<"u"?FormData:null,uv=typeof Blob<"u"?Blob:null,av={isBrowser:!0,classes:{URLSearchParams:sv,FormData:lv,Blob:uv},protocols:["http","https","file","blob","url","data"]},oa=typeof window<"u"&&typeof document<"u",Hu=typeof navigator=="object"&&navigator||void 0,cv=oa&&(!Hu||["ReactNative","NativeScript","NS"].indexOf(Hu.product)<0),fv=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",dv=oa&&window.location.href||"http://localhost",pv=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:oa,hasStandardBrowserEnv:cv,hasStandardBrowserWebWorkerEnv:fv,navigator:Hu,origin:dv},Symbol.toStringTag,{value:"Module"})),Ke={...pv,...av};function hv(r,i){return ws(r,new Ke.classes.URLSearchParams,Object.assign({visitor:function(s,l,c,f){return Ke.isNode&&N.isBuffer(s)?(this.append(l,s.toString("base64")),!1):f.defaultVisitor.apply(this,arguments)}},i))}function mv(r){return N.matchAll(/\w+|\[(\w*)]/g,r).map(i=>i[0]==="[]"?"":i[1]||i[0])}function gv(r){const i={},s=Object.keys(r);let l;const c=s.length;let f;for(l=0;l=s.length;return p=!p&&N.isArray(c)?c.length:p,x?(N.hasOwnProp(c,p)?c[p]=[c[p],l]:c[p]=l,!g):((!c[p]||!N.isObject(c[p]))&&(c[p]=[]),i(s,l,c[p],f)&&N.isArray(c[p])&&(c[p]=gv(c[p])),!g)}if(N.isFormData(r)&&N.isFunction(r.entries)){const s={};return N.forEachEntry(r,(l,c)=>{i(mv(l),c,s,0)}),s}return null}function yv(r,i,s){if(N.isString(r))try{return(i||JSON.parse)(r),N.trim(r)}catch(l){if(l.name!=="SyntaxError")throw l}return(0,JSON.stringify)(r)}const To={transitional:Hp,adapter:["xhr","http","fetch"],transformRequest:[function(i,s){const l=s.getContentType()||"",c=l.indexOf("application/json")>-1,f=N.isObject(i);if(f&&N.isHTMLForm(i)&&(i=new FormData(i)),N.isFormData(i))return c?JSON.stringify(bp(i)):i;if(N.isArrayBuffer(i)||N.isBuffer(i)||N.isStream(i)||N.isFile(i)||N.isBlob(i)||N.isReadableStream(i))return i;if(N.isArrayBufferView(i))return i.buffer;if(N.isURLSearchParams(i))return s.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),i.toString();let g;if(f){if(l.indexOf("application/x-www-form-urlencoded")>-1)return hv(i,this.formSerializer).toString();if((g=N.isFileList(i))||l.indexOf("multipart/form-data")>-1){const x=this.env&&this.env.FormData;return ws(g?{"files[]":i}:i,x&&new x,this.formSerializer)}}return f||c?(s.setContentType("application/json",!1),yv(i)):i}],transformResponse:[function(i){const s=this.transitional||To.transitional,l=s&&s.forcedJSONParsing,c=this.responseType==="json";if(N.isResponse(i)||N.isReadableStream(i))return i;if(i&&N.isString(i)&&(l&&!this.responseType||c)){const p=!(s&&s.silentJSONParsing)&&c;try{return JSON.parse(i)}catch(g){if(p)throw g.name==="SyntaxError"?le.from(g,le.ERR_BAD_RESPONSE,this,null,this.response):g}}return i}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:Ke.classes.FormData,Blob:Ke.classes.Blob},validateStatus:function(i){return i>=200&&i<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};N.forEach(["delete","get","head","post","put","patch"],r=>{To.headers[r]={}});const vv=N.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),wv=r=>{const i={};let s,l,c;return r&&r.split(` +`).forEach(function(p){c=p.indexOf(":"),s=p.substring(0,c).trim().toLowerCase(),l=p.substring(c+1).trim(),!(!s||i[s]&&vv[s])&&(s==="set-cookie"?i[s]?i[s].push(l):i[s]=[l]:i[s]=i[s]?i[s]+", "+l:l)}),i},Id=Symbol("internals");function vo(r){return r&&String(r).trim().toLowerCase()}function es(r){return r===!1||r==null?r:N.isArray(r)?r.map(es):String(r)}function xv(r){const i=Object.create(null),s=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let l;for(;l=s.exec(r);)i[l[1]]=l[2];return i}const Sv=r=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(r.trim());function ju(r,i,s,l,c){if(N.isFunction(l))return l.call(this,i,s);if(c&&(i=s),!!N.isString(i)){if(N.isString(l))return i.indexOf(l)!==-1;if(N.isRegExp(l))return l.test(i)}}function Ev(r){return r.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(i,s,l)=>s.toUpperCase()+l)}function Cv(r,i){const s=N.toCamelCase(" "+i);["get","set","has"].forEach(l=>{Object.defineProperty(r,l+s,{value:function(c,f,p){return this[l].call(this,i,c,f,p)},configurable:!0})})}class ut{constructor(i){i&&this.set(i)}set(i,s,l){const c=this;function f(g,x,v){const S=vo(x);if(!S)throw new Error("header name must be a non-empty string");const A=N.findKey(c,S);(!A||c[A]===void 0||v===!0||v===void 0&&c[A]!==!1)&&(c[A||x]=es(g))}const p=(g,x)=>N.forEach(g,(v,S)=>f(v,S,x));if(N.isPlainObject(i)||i instanceof this.constructor)p(i,s);else if(N.isString(i)&&(i=i.trim())&&!Sv(i))p(wv(i),s);else if(N.isHeaders(i))for(const[g,x]of i.entries())f(x,g,l);else i!=null&&f(s,i,l);return this}get(i,s){if(i=vo(i),i){const l=N.findKey(this,i);if(l){const c=this[l];if(!s)return c;if(s===!0)return xv(c);if(N.isFunction(s))return s.call(this,c,l);if(N.isRegExp(s))return s.exec(c);throw new TypeError("parser must be boolean|regexp|function")}}}has(i,s){if(i=vo(i),i){const l=N.findKey(this,i);return!!(l&&this[l]!==void 0&&(!s||ju(this,this[l],l,s)))}return!1}delete(i,s){const l=this;let c=!1;function f(p){if(p=vo(p),p){const g=N.findKey(l,p);g&&(!s||ju(l,l[g],g,s))&&(delete l[g],c=!0)}}return N.isArray(i)?i.forEach(f):f(i),c}clear(i){const s=Object.keys(this);let l=s.length,c=!1;for(;l--;){const f=s[l];(!i||ju(this,this[f],f,i,!0))&&(delete this[f],c=!0)}return c}normalize(i){const s=this,l={};return N.forEach(this,(c,f)=>{const p=N.findKey(l,f);if(p){s[p]=es(c),delete s[f];return}const g=i?Ev(f):String(f).trim();g!==f&&delete s[f],s[g]=es(c),l[g]=!0}),this}concat(...i){return this.constructor.concat(this,...i)}toJSON(i){const s=Object.create(null);return N.forEach(this,(l,c)=>{l!=null&&l!==!1&&(s[c]=i&&N.isArray(l)?l.join(", "):l)}),s}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([i,s])=>i+": "+s).join(` +`)}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(i){return i instanceof this?i:new this(i)}static concat(i,...s){const l=new this(i);return s.forEach(c=>l.set(c)),l}static accessor(i){const l=(this[Id]=this[Id]={accessors:{}}).accessors,c=this.prototype;function f(p){const g=vo(p);l[g]||(Cv(c,p),l[g]=!0)}return N.isArray(i)?i.forEach(f):f(i),this}}ut.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);N.reduceDescriptors(ut.prototype,({value:r},i)=>{let s=i[0].toUpperCase()+i.slice(1);return{get:()=>r,set(l){this[s]=l}}});N.freezeMethods(ut);function Au(r,i){const s=this||To,l=i||s,c=ut.from(l.headers);let f=l.data;return N.forEach(r,function(g){f=g.call(s,f,c.normalize(),i?i.status:void 0)}),c.normalize(),f}function Vp(r){return!!(r&&r.__CANCEL__)}function Pr(r,i,s){le.call(this,r??"canceled",le.ERR_CANCELED,i,s),this.name="CanceledError"}N.inherits(Pr,le,{__CANCEL__:!0});function Wp(r,i,s){const l=s.config.validateStatus;!s.status||!l||l(s.status)?r(s):i(new le("Request failed with status code "+s.status,[le.ERR_BAD_REQUEST,le.ERR_BAD_RESPONSE][Math.floor(s.status/100)-4],s.config,s.request,s))}function kv(r){const i=/^([-+\w]{1,25})(:?\/\/|:)/.exec(r);return i&&i[1]||""}function jv(r,i){r=r||10;const s=new Array(r),l=new Array(r);let c=0,f=0,p;return i=i!==void 0?i:1e3,function(x){const v=Date.now(),S=l[f];p||(p=v),s[c]=x,l[c]=v;let A=f,R=0;for(;A!==c;)R+=s[A++],A=A%r;if(c=(c+1)%r,c===f&&(f=(f+1)%r),v-p{s=S,c=null,f&&(clearTimeout(f),f=null),r.apply(null,v)};return[(...v)=>{const S=Date.now(),A=S-s;A>=l?p(v,S):(c=v,f||(f=setTimeout(()=>{f=null,p(c)},l-A)))},()=>c&&p(c)]}const ls=(r,i,s=3)=>{let l=0;const c=jv(50,250);return Av(f=>{const p=f.loaded,g=f.lengthComputable?f.total:void 0,x=p-l,v=c(x),S=p<=g;l=p;const A={loaded:p,total:g,progress:g?p/g:void 0,bytes:x,rate:v||void 0,estimated:v&&g&&S?(g-p)/v:void 0,event:f,lengthComputable:g!=null,[i?"download":"upload"]:!0};r(A)},s)},Nd=(r,i)=>{const s=r!=null;return[l=>i[0]({lengthComputable:s,total:r,loaded:l}),i[1]]},Od=r=>(...i)=>N.asap(()=>r(...i)),Rv=Ke.hasStandardBrowserEnv?((r,i)=>s=>(s=new URL(s,Ke.origin),r.protocol===s.protocol&&r.host===s.host&&(i||r.port===s.port)))(new URL(Ke.origin),Ke.navigator&&/(msie|trident)/i.test(Ke.navigator.userAgent)):()=>!0,Pv=Ke.hasStandardBrowserEnv?{write(r,i,s,l,c,f){const p=[r+"="+encodeURIComponent(i)];N.isNumber(s)&&p.push("expires="+new Date(s).toGMTString()),N.isString(l)&&p.push("path="+l),N.isString(c)&&p.push("domain="+c),f===!0&&p.push("secure"),document.cookie=p.join("; ")},read(r){const i=document.cookie.match(new RegExp("(^|;\\s*)("+r+")=([^;]*)"));return i?decodeURIComponent(i[3]):null},remove(r){this.write(r,"",Date.now()-864e5)}}:{write(){},read(){return null},remove(){}};function _v(r){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(r)}function Tv(r,i){return i?r.replace(/\/?\/$/,"")+"/"+i.replace(/^\/+/,""):r}function Yp(r,i){return r&&!_v(i)?Tv(r,i):i}const Ld=r=>r instanceof ut?{...r}:r;function Wn(r,i){i=i||{};const s={};function l(v,S,A,R){return N.isPlainObject(v)&&N.isPlainObject(S)?N.merge.call({caseless:R},v,S):N.isPlainObject(S)?N.merge({},S):N.isArray(S)?S.slice():S}function c(v,S,A,R){if(N.isUndefined(S)){if(!N.isUndefined(v))return l(void 0,v,A,R)}else return l(v,S,A,R)}function f(v,S){if(!N.isUndefined(S))return l(void 0,S)}function p(v,S){if(N.isUndefined(S)){if(!N.isUndefined(v))return l(void 0,v)}else return l(void 0,S)}function g(v,S,A){if(A in i)return l(v,S);if(A in r)return l(void 0,v)}const x={url:f,method:f,data:f,baseURL:p,transformRequest:p,transformResponse:p,paramsSerializer:p,timeout:p,timeoutMessage:p,withCredentials:p,withXSRFToken:p,adapter:p,responseType:p,xsrfCookieName:p,xsrfHeaderName:p,onUploadProgress:p,onDownloadProgress:p,decompress:p,maxContentLength:p,maxBodyLength:p,beforeRedirect:p,transport:p,httpAgent:p,httpsAgent:p,cancelToken:p,socketPath:p,responseEncoding:p,validateStatus:g,headers:(v,S,A)=>c(Ld(v),Ld(S),A,!0)};return N.forEach(Object.keys(Object.assign({},r,i)),function(S){const A=x[S]||c,R=A(r[S],i[S],S);N.isUndefined(R)&&A!==g||(s[S]=R)}),s}const qp=r=>{const i=Wn({},r);let{data:s,withXSRFToken:l,xsrfHeaderName:c,xsrfCookieName:f,headers:p,auth:g}=i;i.headers=p=ut.from(p),i.url=$p(Yp(i.baseURL,i.url),r.params,r.paramsSerializer),g&&p.set("Authorization","Basic "+btoa((g.username||"")+":"+(g.password?unescape(encodeURIComponent(g.password)):"")));let x;if(N.isFormData(s)){if(Ke.hasStandardBrowserEnv||Ke.hasStandardBrowserWebWorkerEnv)p.setContentType(void 0);else if((x=p.getContentType())!==!1){const[v,...S]=x?x.split(";").map(A=>A.trim()).filter(Boolean):[];p.setContentType([v||"multipart/form-data",...S].join("; "))}}if(Ke.hasStandardBrowserEnv&&(l&&N.isFunction(l)&&(l=l(i)),l||l!==!1&&Rv(i.url))){const v=c&&f&&Pv.read(f);v&&p.set(c,v)}return i},Iv=typeof XMLHttpRequest<"u",Nv=Iv&&function(r){return new Promise(function(s,l){const c=qp(r);let f=c.data;const p=ut.from(c.headers).normalize();let{responseType:g,onUploadProgress:x,onDownloadProgress:v}=c,S,A,R,I,_;function C(){I&&I(),_&&_(),c.cancelToken&&c.cancelToken.unsubscribe(S),c.signal&&c.signal.removeEventListener("abort",S)}let O=new XMLHttpRequest;O.open(c.method.toUpperCase(),c.url,!0),O.timeout=c.timeout;function F(){if(!O)return;const V=ut.from("getAllResponseHeaders"in O&&O.getAllResponseHeaders()),H={data:!g||g==="text"||g==="json"?O.responseText:O.response,status:O.status,statusText:O.statusText,headers:V,config:r,request:O};Wp(function(b){s(b),C()},function(b){l(b),C()},H),O=null}"onloadend"in O?O.onloadend=F:O.onreadystatechange=function(){!O||O.readyState!==4||O.status===0&&!(O.responseURL&&O.responseURL.indexOf("file:")===0)||setTimeout(F)},O.onabort=function(){O&&(l(new le("Request aborted",le.ECONNABORTED,r,O)),O=null)},O.onerror=function(){l(new le("Network Error",le.ERR_NETWORK,r,O)),O=null},O.ontimeout=function(){let Q=c.timeout?"timeout of "+c.timeout+"ms exceeded":"timeout exceeded";const H=c.transitional||Hp;c.timeoutErrorMessage&&(Q=c.timeoutErrorMessage),l(new le(Q,H.clarifyTimeoutError?le.ETIMEDOUT:le.ECONNABORTED,r,O)),O=null},f===void 0&&p.setContentType(null),"setRequestHeader"in O&&N.forEach(p.toJSON(),function(Q,H){O.setRequestHeader(H,Q)}),N.isUndefined(c.withCredentials)||(O.withCredentials=!!c.withCredentials),g&&g!=="json"&&(O.responseType=c.responseType),v&&([R,_]=ls(v,!0),O.addEventListener("progress",R)),x&&O.upload&&([A,I]=ls(x),O.upload.addEventListener("progress",A),O.upload.addEventListener("loadend",I)),(c.cancelToken||c.signal)&&(S=V=>{O&&(l(!V||V.type?new Pr(null,r,O):V),O.abort(),O=null)},c.cancelToken&&c.cancelToken.subscribe(S),c.signal&&(c.signal.aborted?S():c.signal.addEventListener("abort",S)));const B=kv(c.url);if(B&&Ke.protocols.indexOf(B)===-1){l(new le("Unsupported protocol "+B+":",le.ERR_BAD_REQUEST,r));return}O.send(f||null)})},Ov=(r,i)=>{const{length:s}=r=r?r.filter(Boolean):[];if(i||s){let l=new AbortController,c;const f=function(v){if(!c){c=!0,g();const S=v instanceof Error?v:this.reason;l.abort(S instanceof le?S:new Pr(S instanceof Error?S.message:S))}};let p=i&&setTimeout(()=>{p=null,f(new le(`timeout ${i} of ms exceeded`,le.ETIMEDOUT))},i);const g=()=>{r&&(p&&clearTimeout(p),p=null,r.forEach(v=>{v.unsubscribe?v.unsubscribe(f):v.removeEventListener("abort",f)}),r=null)};r.forEach(v=>v.addEventListener("abort",f));const{signal:x}=l;return x.unsubscribe=()=>N.asap(g),x}},Lv=function*(r,i){let s=r.byteLength;if(s{const c=Dv(r,i);let f=0,p,g=x=>{p||(p=!0,l&&l(x))};return new ReadableStream({async pull(x){try{const{done:v,value:S}=await c.next();if(v){g(),x.close();return}let A=S.byteLength;if(s){let R=f+=A;s(R)}x.enqueue(new Uint8Array(S))}catch(v){throw g(v),v}},cancel(x){return g(x),c.return()}},{highWaterMark:2})},xs=typeof fetch=="function"&&typeof Request=="function"&&typeof Response=="function",Qp=xs&&typeof ReadableStream=="function",zv=xs&&(typeof TextEncoder=="function"?(r=>i=>r.encode(i))(new TextEncoder):async r=>new Uint8Array(await new Response(r).arrayBuffer())),Gp=(r,...i)=>{try{return!!r(...i)}catch{return!1}},Uv=Qp&&Gp(()=>{let r=!1;const i=new Request(Ke.origin,{body:new ReadableStream,method:"POST",get duplex(){return r=!0,"half"}}).headers.has("Content-Type");return r&&!i}),Md=64*1024,bu=Qp&&Gp(()=>N.isReadableStream(new Response("").body)),us={stream:bu&&(r=>r.body)};xs&&(r=>{["text","arrayBuffer","blob","formData","stream"].forEach(i=>{!us[i]&&(us[i]=N.isFunction(r[i])?s=>s[i]():(s,l)=>{throw new le(`Response type '${i}' is not supported`,le.ERR_NOT_SUPPORT,l)})})})(new Response);const Fv=async r=>{if(r==null)return 0;if(N.isBlob(r))return r.size;if(N.isSpecCompliantForm(r))return(await new Request(Ke.origin,{method:"POST",body:r}).arrayBuffer()).byteLength;if(N.isArrayBufferView(r)||N.isArrayBuffer(r))return r.byteLength;if(N.isURLSearchParams(r)&&(r=r+""),N.isString(r))return(await zv(r)).byteLength},Bv=async(r,i)=>{const s=N.toFiniteNumber(r.getContentLength());return s??Fv(i)},$v=xs&&(async r=>{let{url:i,method:s,data:l,signal:c,cancelToken:f,timeout:p,onDownloadProgress:g,onUploadProgress:x,responseType:v,headers:S,withCredentials:A="same-origin",fetchOptions:R}=qp(r);v=v?(v+"").toLowerCase():"text";let I=Ov([c,f&&f.toAbortSignal()],p),_;const C=I&&I.unsubscribe&&(()=>{I.unsubscribe()});let O;try{if(x&&Uv&&s!=="get"&&s!=="head"&&(O=await Bv(S,l))!==0){let H=new Request(i,{method:"POST",body:l,duplex:"half"}),L;if(N.isFormData(l)&&(L=H.headers.get("content-type"))&&S.setContentType(L),H.body){const[b,re]=Nd(O,ls(Od(x)));l=Dd(H.body,Md,b,re)}}N.isString(A)||(A=A?"include":"omit");const F="credentials"in Request.prototype;_=new Request(i,{...R,signal:I,method:s.toUpperCase(),headers:S.normalize().toJSON(),body:l,duplex:"half",credentials:F?A:void 0});let B=await fetch(_);const V=bu&&(v==="stream"||v==="response");if(bu&&(g||V&&C)){const H={};["status","statusText","headers"].forEach(ye=>{H[ye]=B[ye]});const L=N.toFiniteNumber(B.headers.get("content-length")),[b,re]=g&&Nd(L,ls(Od(g),!0))||[];B=new Response(Dd(B.body,Md,b,()=>{re&&re(),C&&C()}),H)}v=v||"text";let Q=await us[N.findKey(us,v)||"text"](B,r);return!V&&C&&C(),await new Promise((H,L)=>{Wp(H,L,{data:Q,headers:ut.from(B.headers),status:B.status,statusText:B.statusText,config:r,request:_})})}catch(F){throw C&&C(),F&&F.name==="TypeError"&&/fetch/i.test(F.message)?Object.assign(new le("Network Error",le.ERR_NETWORK,r,_),{cause:F.cause||F}):le.from(F,F&&F.code,r,_)}}),Vu={http:nv,xhr:Nv,fetch:$v};N.forEach(Vu,(r,i)=>{if(r){try{Object.defineProperty(r,"name",{value:i})}catch{}Object.defineProperty(r,"adapterName",{value:i})}});const zd=r=>`- ${r}`,Hv=r=>N.isFunction(r)||r===null||r===!1,Kp={getAdapter:r=>{r=N.isArray(r)?r:[r];const{length:i}=r;let s,l;const c={};for(let f=0;f`adapter ${g} `+(x===!1?"is not supported by the environment":"is not available in the build"));let p=i?f.length>1?`since : +`+f.map(zd).join(` +`):" "+zd(f[0]):"as no adapter specified";throw new le("There is no suitable adapter to dispatch the request "+p,"ERR_NOT_SUPPORT")}return l},adapters:Vu};function Ru(r){if(r.cancelToken&&r.cancelToken.throwIfRequested(),r.signal&&r.signal.aborted)throw new Pr(null,r)}function Ud(r){return Ru(r),r.headers=ut.from(r.headers),r.data=Au.call(r,r.transformRequest),["post","put","patch"].indexOf(r.method)!==-1&&r.headers.setContentType("application/x-www-form-urlencoded",!1),Kp.getAdapter(r.adapter||To.adapter)(r).then(function(l){return Ru(r),l.data=Au.call(r,r.transformResponse,l),l.headers=ut.from(l.headers),l},function(l){return Vp(l)||(Ru(r),l&&l.response&&(l.response.data=Au.call(r,r.transformResponse,l.response),l.response.headers=ut.from(l.response.headers))),Promise.reject(l)})}const Xp="1.7.9",Ss={};["object","boolean","number","function","string","symbol"].forEach((r,i)=>{Ss[r]=function(l){return typeof l===r||"a"+(i<1?"n ":" ")+r}});const Fd={};Ss.transitional=function(i,s,l){function c(f,p){return"[Axios v"+Xp+"] Transitional option '"+f+"'"+p+(l?". "+l:"")}return(f,p,g)=>{if(i===!1)throw new le(c(p," has been removed"+(s?" in "+s:"")),le.ERR_DEPRECATED);return s&&!Fd[p]&&(Fd[p]=!0,console.warn(c(p," has been deprecated since v"+s+" and will be removed in the near future"))),i?i(f,p,g):!0}};Ss.spelling=function(i){return(s,l)=>(console.warn(`${l} is likely a misspelling of ${i}`),!0)};function bv(r,i,s){if(typeof r!="object")throw new le("options must be an object",le.ERR_BAD_OPTION_VALUE);const l=Object.keys(r);let c=l.length;for(;c-- >0;){const f=l[c],p=i[f];if(p){const g=r[f],x=g===void 0||p(g,f,r);if(x!==!0)throw new le("option "+f+" must be "+x,le.ERR_BAD_OPTION_VALUE);continue}if(s!==!0)throw new le("Unknown option "+f,le.ERR_BAD_OPTION)}}const ts={assertOptions:bv,validators:Ss},Vt=ts.validators;class Hn{constructor(i){this.defaults=i,this.interceptors={request:new Td,response:new Td}}async request(i,s){try{return await this._request(i,s)}catch(l){if(l instanceof Error){let c={};Error.captureStackTrace?Error.captureStackTrace(c):c=new Error;const f=c.stack?c.stack.replace(/^.+\n/,""):"";try{l.stack?f&&!String(l.stack).endsWith(f.replace(/^.+\n.+\n/,""))&&(l.stack+=` +`+f):l.stack=f}catch{}}throw l}}_request(i,s){typeof i=="string"?(s=s||{},s.url=i):s=i||{},s=Wn(this.defaults,s);const{transitional:l,paramsSerializer:c,headers:f}=s;l!==void 0&&ts.assertOptions(l,{silentJSONParsing:Vt.transitional(Vt.boolean),forcedJSONParsing:Vt.transitional(Vt.boolean),clarifyTimeoutError:Vt.transitional(Vt.boolean)},!1),c!=null&&(N.isFunction(c)?s.paramsSerializer={serialize:c}:ts.assertOptions(c,{encode:Vt.function,serialize:Vt.function},!0)),ts.assertOptions(s,{baseUrl:Vt.spelling("baseURL"),withXsrfToken:Vt.spelling("withXSRFToken")},!0),s.method=(s.method||this.defaults.method||"get").toLowerCase();let p=f&&N.merge(f.common,f[s.method]);f&&N.forEach(["delete","get","head","post","put","patch","common"],_=>{delete f[_]}),s.headers=ut.concat(p,f);const g=[];let x=!0;this.interceptors.request.forEach(function(C){typeof C.runWhen=="function"&&C.runWhen(s)===!1||(x=x&&C.synchronous,g.unshift(C.fulfilled,C.rejected))});const v=[];this.interceptors.response.forEach(function(C){v.push(C.fulfilled,C.rejected)});let S,A=0,R;if(!x){const _=[Ud.bind(this),void 0];for(_.unshift.apply(_,g),_.push.apply(_,v),R=_.length,S=Promise.resolve(s);A{if(!l._listeners)return;let f=l._listeners.length;for(;f-- >0;)l._listeners[f](c);l._listeners=null}),this.promise.then=c=>{let f;const p=new Promise(g=>{l.subscribe(g),f=g}).then(c);return p.cancel=function(){l.unsubscribe(f)},p},i(function(f,p,g){l.reason||(l.reason=new Pr(f,p,g),s(l.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(i){if(this.reason){i(this.reason);return}this._listeners?this._listeners.push(i):this._listeners=[i]}unsubscribe(i){if(!this._listeners)return;const s=this._listeners.indexOf(i);s!==-1&&this._listeners.splice(s,1)}toAbortSignal(){const i=new AbortController,s=l=>{i.abort(l)};return this.subscribe(s),i.signal.unsubscribe=()=>this.unsubscribe(s),i.signal}static source(){let i;return{token:new ia(function(c){i=c}),cancel:i}}}function Vv(r){return function(s){return r.apply(null,s)}}function Wv(r){return N.isObject(r)&&r.isAxiosError===!0}const Wu={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Wu).forEach(([r,i])=>{Wu[i]=r});function Jp(r){const i=new Hn(r),s=_p(Hn.prototype.request,i);return N.extend(s,Hn.prototype,i,{allOwnKeys:!0}),N.extend(s,i,null,{allOwnKeys:!0}),s.create=function(c){return Jp(Wn(r,c))},s}const De=Jp(To);De.Axios=Hn;De.CanceledError=Pr;De.CancelToken=ia;De.isCancel=Vp;De.VERSION=Xp;De.toFormData=ws;De.AxiosError=le;De.Cancel=De.CanceledError;De.all=function(i){return Promise.all(i)};De.spread=Vv;De.isAxiosError=Wv;De.mergeConfig=Wn;De.AxiosHeaders=ut;De.formToJSON=r=>bp(N.isHTMLForm(r)?new FormData(r):r);De.getAdapter=Kp.getAdapter;De.HttpStatusCode=Wu;De.default=De;const Yv={apiBaseUrl:"/api"};class qv{constructor(){ed(this,"events",{})}on(i,s){return this.events[i]||(this.events[i]=[]),this.events[i].push(s),()=>this.off(i,s)}off(i,s){this.events[i]&&(this.events[i]=this.events[i].filter(l=>l!==s))}emit(i,...s){this.events[i]&&this.events[i].forEach(l=>{l(...s)})}}const as=new qv,Je=De.create({baseURL:Yv.apiBaseUrl,headers:{"Content-Type":"application/json"}});Je.interceptors.response.use(r=>r,r=>{var s,l,c;const i=(s=r.response)==null?void 0:s.data;if(i){const f=(c=(l=r.response)==null?void 0:l.headers)==null?void 0:c["discodeit-request-id"];f&&(i.requestId=f),r.response.data=i}return as.emit("api-error",r),r.response&&r.response.status===401&&as.emit("auth-error"),Promise.reject(r)});const Qv=()=>Je.defaults.baseURL,Gv=async(r,i)=>{const s={username:r,password:i};return(await Je.post("/auth/login",s)).data},Kv=async r=>(await Je.post("/users",r,{headers:{"Content-Type":"multipart/form-data"}})).data,Bd=r=>{let i;const s=new Set,l=(v,S)=>{const A=typeof v=="function"?v(i):v;if(!Object.is(A,i)){const R=i;i=S??(typeof A!="object"||A===null)?A:Object.assign({},i,A),s.forEach(I=>I(i,R))}},c=()=>i,g={setState:l,getState:c,getInitialState:()=>x,subscribe:v=>(s.add(v),()=>s.delete(v))},x=i=r(l,c,g);return g},Xv=r=>r?Bd(r):Bd,Jv=r=>r;function Zv(r,i=Jv){const s=gt.useSyncExternalStore(r.subscribe,()=>i(r.getState()),()=>i(r.getInitialState()));return gt.useDebugValue(s),s}const $d=r=>{const i=Xv(r),s=l=>Zv(i,l);return Object.assign(s,i),s},_r=r=>r?$d(r):$d,e0=async(r,i)=>(await Je.patch(`/users/${r}`,i,{headers:{"Content-Type":"multipart/form-data"}})).data,t0=async()=>(await Je.get("/users")).data,n0=async r=>(await Je.patch(`/users/${r}/userStatus`,{newLastActiveAt:new Date().toISOString()})).data,nn=_r(r=>({users:[],fetchUsers:async()=>{try{const i=await t0();r({users:i})}catch(i){console.error("사용자 목록 조회 실패:",i)}},updateUserStatus:async i=>{try{await n0(i)}catch(s){console.error("사용자 상태 업데이트 실패:",s)}}}));function Zp(r,i){let s;try{s=r()}catch{return}return{getItem:c=>{var f;const p=x=>x===null?null:JSON.parse(x,void 0),g=(f=s.getItem(c))!=null?f:null;return g instanceof Promise?g.then(p):p(g)},setItem:(c,f)=>s.setItem(c,JSON.stringify(f,void 0)),removeItem:c=>s.removeItem(c)}}const Yu=r=>i=>{try{const s=r(i);return s instanceof Promise?s:{then(l){return Yu(l)(s)},catch(l){return this}}}catch(s){return{then(l){return this},catch(l){return Yu(l)(s)}}}},r0=(r,i)=>(s,l,c)=>{let f={storage:Zp(()=>localStorage),partialize:C=>C,version:0,merge:(C,O)=>({...O,...C}),...i},p=!1;const g=new Set,x=new Set;let v=f.storage;if(!v)return r((...C)=>{console.warn(`[zustand persist middleware] Unable to update item '${f.name}', the given storage is currently unavailable.`),s(...C)},l,c);const S=()=>{const C=f.partialize({...l()});return v.setItem(f.name,{state:C,version:f.version})},A=c.setState;c.setState=(C,O)=>{A(C,O),S()};const R=r((...C)=>{s(...C),S()},l,c);c.getInitialState=()=>R;let I;const _=()=>{var C,O;if(!v)return;p=!1,g.forEach(B=>{var V;return B((V=l())!=null?V:R)});const F=((O=f.onRehydrateStorage)==null?void 0:O.call(f,(C=l())!=null?C:R))||void 0;return Yu(v.getItem.bind(v))(f.name).then(B=>{if(B)if(typeof B.version=="number"&&B.version!==f.version){if(f.migrate){const V=f.migrate(B.state,B.version);return V instanceof Promise?V.then(Q=>[!0,Q]):[!0,V]}console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return[!1,B.state];return[!1,void 0]}).then(B=>{var V;const[Q,H]=B;if(I=f.merge(H,(V=l())!=null?V:R),s(I,!0),Q)return S()}).then(()=>{F==null||F(I,void 0),I=l(),p=!0,x.forEach(B=>B(I))}).catch(B=>{F==null||F(void 0,B)})};return c.persist={setOptions:C=>{f={...f,...C},C.storage&&(v=C.storage)},clearStorage:()=>{v==null||v.removeItem(f.name)},getOptions:()=>f,rehydrate:()=>_(),hasHydrated:()=>p,onHydrate:C=>(g.add(C),()=>{g.delete(C)}),onFinishHydration:C=>(x.add(C),()=>{x.delete(C)})},f.skipHydration||_(),I||R},o0=r0,vt=_r()(o0(r=>({currentUserId:null,setCurrentUser:i=>r({currentUserId:i.id}),logout:()=>{const i=vt.getState().currentUserId;i&&nn.getState().updateUserStatus(i),r({currentUserId:null})},updateUser:async(i,s)=>{try{const l=await e0(i,s);return await nn.getState().fetchUsers(),l}catch(l){throw console.error("사용자 정보 수정 실패:",l),l}}}),{name:"user-storage",storage:Zp(()=>sessionStorage)})),ee={colors:{brand:{primary:"#5865F2",hover:"#4752C4"},background:{primary:"#1a1a1a",secondary:"#2a2a2a",tertiary:"#333333",input:"#40444B",hover:"rgba(255, 255, 255, 0.1)"},text:{primary:"#ffffff",secondary:"#cccccc",muted:"#999999"},status:{online:"#43b581",idle:"#faa61a",dnd:"#f04747",offline:"#747f8d",error:"#ED4245"},border:{primary:"#404040"}}},eh=T.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`,th=T.div` + background: ${ee.colors.background.primary}; + padding: 32px; + border-radius: 8px; + width: 440px; + + h2 { + color: ${ee.colors.text.primary}; + margin-bottom: 24px; + font-size: 24px; + font-weight: bold; + } + + form { + display: flex; + flex-direction: column; + gap: 16px; + } +`,ko=T.input` + width: 100%; + padding: 10px; + border-radius: 4px; + background: ${ee.colors.background.input}; + border: none; + color: ${ee.colors.text.primary}; + font-size: 16px; + + &::placeholder { + color: ${ee.colors.text.muted}; + } + + &:focus { + outline: none; + } +`,nh=T.button` + width: 100%; + padding: 12px; + border-radius: 4px; + background: ${ee.colors.brand.primary}; + color: white; + font-size: 16px; + font-weight: 500; + border: none; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background: ${ee.colors.brand.hover}; + } +`,rh=T.div` + color: ${ee.colors.status.error}; + font-size: 14px; + text-align: center; +`,i0=T.p` + text-align: center; + margin-top: 16px; + color: ${({theme:r})=>r.colors.text.muted}; + font-size: 14px; +`,s0=T.span` + color: ${({theme:r})=>r.colors.brand.primary}; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +`,Vi=T.div` + margin-bottom: 20px; +`,Wi=T.label` + display: block; + color: ${({theme:r})=>r.colors.text.muted}; + font-size: 12px; + font-weight: 700; + margin-bottom: 8px; +`,Pu=T.span` + color: ${({theme:r})=>r.colors.status.error}; +`,l0=T.div` + display: flex; + flex-direction: column; + align-items: center; + margin: 10px 0; +`,u0=T.img` + width: 80px; + height: 80px; + border-radius: 50%; + margin-bottom: 10px; + object-fit: cover; +`,a0=T.input` + display: none; +`,c0=T.label` + color: ${({theme:r})=>r.colors.brand.primary}; + cursor: pointer; + font-size: 14px; + + &:hover { + text-decoration: underline; + } +`,f0=T.span` + color: ${({theme:r})=>r.colors.brand.primary}; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +`,d0=T(f0)` + display: block; + text-align: center; + margin-top: 16px; +`,zt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwCAYAAAA+VemSAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAw2SURBVHgB7d3PT1XpHcfxBy5g6hipSMolGViACThxJDbVRZ2FXejKlf9h/4GmC1fTRdkwC8fE0JgyJuICFkCjEA04GeZe6P0cPC0698I95zzPc57v5f1K6DSto3A8n/v9nufXGfrr338+dgBMGnYAzCLAgGEEGDCMAAOGEWDAMAIMGEaAAcMIMGAYAQYMI8CAYQQYMIwAA4YRYMAwAgwYRoABwwgwYBgBBgwjwIBhBBgwjAADhhFgwDACDBhGgAHDCDBgGAEGDCPAgGEEGDCMAAOGEWDAMAIMGEaAAcMIMGAYAQYMI8CAYQQYMIwAA4YRYMAwAgwYRoABwwgwYBgBBgwjwIBhBBgwjAADhhFgwDACDBhGgAHDCDBgGAEGDCPAgGEEGDCMAAOGEWDAMAIMGEaAAcMIMGAYAQYMI8CAYQQYMIwAA4YRYMAwAgwYRoABwwgwYBgBBgwbcTDvyuWh//33w1/1dexwMRBgYxTW5vVh9/vxYTcxPpR9jY0OffZrdt8fu82ttlvfbLv9j4R5kBHgxCmcE1eH3NfTDTc7PfxZte3lJNgjbmlxxK3+1HKrr1oOg4kAJ0pVdnG+4ZqTw7+psEUoxF91Qv/Di1+db/q+ZpvD7g+T6gb04XLyv6mF3//osuqvTmDn3RGdQCAEOCG6+W/ONdzNTnCrhPZLN2Yb2T99hVhdwOLcSOf37f7hknUN4yedgLoGeb3Rdv/qdAIE2S8CnIDzAuGDQrzXeTZee1OtndaHy9LCSOHvU3++vv693nLPX9LS+0KAa6QQLC2o4sb5a1A7rYGtMqPU+l7v3hpx85+qeVnfdH7W2c7z/Pcrh1RjD5gHromq2JOHY9HCK2Ojzk1dL1fhH90fqxzenDoO/X79DMjhbAQ4Mg1OPXl4KauGodrls6j6FaXKq+dZn/IQ13ENBgkBjiRvQR99V2/lmZos9lc+PxOuxdd1uL3gp6pfVDwDR6Ab9cG9Me9VLAZ1CiHpmXhz6yibakJxVODAZpoN9/iBzfCq+sboFkJ/SAwyrlxAujE1WJWSIiO/sYKlxSpTnbEBqnBxVOBA9LybWnjloM8An6ysitc1NCe5FcvgqgVw/85o1OmhItY32n39uqnJuC3/FAEuhavmmcLra77UN7XP2322qRNX494aqvgojqvmUcrhFa1+6tdXkae6tMiEhR3FEWBPNOCTcni1rZCli4OHAHuQ4mjzaewJHlxMI1Wked5Uw7v99ijbwqd/FnVQQ7WmQyiOAFegZ7a736ZzCU820h+7nbfHbnO7XSq4p3+vmHbfMwdcBgGuoO4dNQrZxtaR+08nqNueT73Y2D7qTIW5aLRXGcUR4JL03FtHeBXa9Y2jyhX2PHudiqg/K9ZuoY3t/uan8TkCXIKCG/u5V2Fae9N2a+vtKO2tjqfVnxfj5zw5O4sWugwCXIJa51hiB/e0tfVWdkZX6CrMCHl5BLigWDt0RCc6rrxo1XZQu6rw6qt2tq47FD0G9Lu8E79FgAvIWucIO3QU2B9ftpK4sVWFZ5rDQTYbqHUOcdztRcJCjgLUToauvrqpny4fJlWVlp/5P4BOH1IcbFcdAe6Tght6h5FeiaLwpnZTq5VW2HzN1eYfUoS3OgLcp9sL4cOrkKT6YrI8dFUHnDQYR3j94Rm4D9kLxQLuV009vKdpXbXae00vFdm8UWVZJ3ojwH3QcS+hnn1VifSMaemVoPqeVzqDT6rG2oivQS5dH33l70ZS262w7n04yhae8MrTMAhwH0KNPFsfyNH3vd+pxkwD1Ydn4HOodQ5VfTXHyrMgqiDA55ibCbNJX1VLc6xAFQT4HCEGr9Q6s3wQPhDgM4RqnzWVQusMHwjwGTS66puCS/WFLwT4DCHOKia88IkA96BjTkOcVbzDQgZ4RIB7CBFejTzz7AufCHAPWn3lGwse4BsB7uGa5wqcLS3k7XvwjAD3cOWy84pnX4RAgHvw/QzMLhyEQIC7CLF4Y4+DyxEAAe4iRIB3PzD6DP8IcBejnncPagCL/bAIgQB34fsc5P2PtM8IgwBHcMjJqQiEAHfBm+JhBQGO4IDlkwiEAHdx2PIbuFhv+MPFQ4C7ODx0Xo2OOiAIAhwBz9QIhQB34XvOlhYaoRDgLg5+dl7pcACqMEIgwF2EWDV1bZwAwz8C3IVOzfAd4omrXGr4x13Vg++jb6YmudTwj7uqh733fgOsM6YZzIJvBLiH3Q/+NyDMB3pNCy4u3k7Yw+57/wNZM9PDbu2NGwjqJiauDrmvpxufXiv6+f+v63fw8SjrZDgLLBwC3INO0NBAls+2V220jurZNXw6h8K6ODfibsye/UjQnNR/nnQcGk/IX/DNsbp+EeAetAVQVaQ56fe5dXGu4X54YTPASwsj7uZ8o/CHmkJ/Y7aRfb3eaBNkj3gGPsNOgNZPN7G1RR36fh8/uJS96LxqR6Kf/9H9MRa2eEKAz7C5FaZS3l6w0/goaArchMeFKPkHwrVxbr+quIJn0LNqiFZPVSjEmx98U7UNVS016PWXe6NU4ooI8DnWN8O8DuX+H0eTnxdeWgjb7uv3/vMd9lpWQYDPEep9Rrp5by+kOy+s7+/mfPhWXyPzFrqRVHHlzpFPgYTwTScg87NphjhmZdTgGMohwH1YexPupdx3b40mN5ij6tuMuHabKlweV60PGo0OdTB7ioM5WjEWW5PNHqVw1fq09ibcu33zqZpUQjzTjN/Ws1urHK5an9bWW0Ffj5JSiOv4HiaYEy6Fq9YnLa1cfRWuCku+wOHmXL2DOnUEmGOHyiHABagKh17Dqxv57rcj7k+3RpKfJ0b9CHBBKy/ivOhIU0yPH4xdqD3EV37HB1ZRBLignc6c8MZW2FY6p5ZSK7b0bNyMOM3CTiE7CHAJz1+2or7vV1Msj74by4IcoyKHOMygH4fhptsHFgEuQRXqx5fx7zYFWRX5ycNL2UqpUFV5512cDuNLvAS9ONawlaQ10jpSJsZ64S+d3iCvm3777XGntW9nx9fsfqh+JK5+Nq0Qi43WvTgCXMHqq5abma53g75Gqmen9fX/alz1CBtNmenfj7k6yvIxQ3Wiha5AN/r3K4fJtX55hVarvVTy8AB9OMV0GGdwf+AQ4IpU4f75LN27Tzt9HtwbKzynrNF2zXvHsvOWClwGAfZAN18dg1r9UnuthSFF6WeK1doS4HIIsCeqVrHbziLUUpdZornc6S5iDC5p8A3FEWCPVn9KO8RlTpVUeJ8u/xLsUAPR780UUjkE2LOUQ6x11jPN4n/l+WDdaqDznEOdO3YREOAAFOJUn4mrTA3p51KQNU/sM8g8/5bHPHAgeibWAND9O2mdtlF147yCm2/o0IeBXlyuAwDKfjDotBMWcJRHBQ5IlUUVa1Bv0O1squnkVSllvd5kAXQVBDiwfBAo5pyqFbo2od5+cVEQ4Ag0CKRnYrWedVfjlLqBlEfsrSDAEWnwJx8Eqsve+zQCrA+SOq/DoCDAkeWDQE+X63k23txKIzRUXz8IcE00Qv23f/wSta3Odim9q/+Zc6Pz3Ev19YNppJrpRtaXXrGinUMhp5zUvqfg+Uu2HvlCgBORB1nzqYtzDTc77ffoHC3CSGEAS4N5zPv6Q4ATo7lVfV253MoWXegMrKob6xWaFKax9PzNdJpfBDhRqlL7n6qy2mqFWeuY9QaDfttsfRCoXd1NYOS5rnPEBh0BNuB0mGVifOgk1Ncb2VJGbVLIdxnp12qqaHO7HXQHURH6ngZ5RVqdCLBBqqj62jCwiknbBJefEd5QCDCCUWgV3hRa+EFFgBEEbXMcBBjeabR55UWLUzYiIMDwRoHVK1iZKoqHAMMLqm49CDAqyxefID42MwCGEWDAMAIMGEaAAcMIMGAYAQYMI8CAYQQYMIwAA4YRYMAwAgwYRoABwwgwYBgBBgwjwIBhBBgwjAADhhFgwDACDBhGgAHDCDBgGAEGDCPAgGEEGDCMAAOGEWDAMAIMGEaAAcMIMGAYAQYMI8CAYQQYMIwAA4YRYMAwAgwYRoABwwgwYBgBBgwjwIBhBBgwjAADhhFgwDACDBhGgAHDCDBgGAEGDCPAgGEEGDCMAAOGEWDAMAIMGEaAAcMIMGAYAQYMI8CAYQQYMIwAA4YRYMAwAgwYRoABwwgwYBgBBgwjwIBhBBgwjAADhv0XZkN9IbEGbp4AAAAASUVORK5CYII=",p0=({isOpen:r,onClose:i})=>{const[s,l]=ie.useState(""),[c,f]=ie.useState(""),[p,g]=ie.useState(""),[x,v]=ie.useState(null),[S,A]=ie.useState(null),[R,I]=ie.useState(""),_=vt(F=>F.setCurrentUser),C=F=>{var V;const B=(V=F.target.files)==null?void 0:V[0];if(B){v(B);const Q=new FileReader;Q.onloadend=()=>{A(Q.result)},Q.readAsDataURL(B)}},O=async F=>{F.preventDefault(),I("");try{const B=new FormData;B.append("userCreateRequest",new Blob([JSON.stringify({email:s,username:c,password:p})],{type:"application/json"})),x&&B.append("profile",x);const V=await Kv(B);_(V),i()}catch{I("회원가입에 실패했습니다.")}};return r?h.jsx(eh,{children:h.jsxs(th,{children:[h.jsx("h2",{children:"계정 만들기"}),h.jsxs("form",{onSubmit:O,children:[h.jsxs(Vi,{children:[h.jsxs(Wi,{children:["이메일 ",h.jsx(Pu,{children:"*"})]}),h.jsx(ko,{type:"email",value:s,onChange:F=>l(F.target.value),required:!0})]}),h.jsxs(Vi,{children:[h.jsxs(Wi,{children:["사용자명 ",h.jsx(Pu,{children:"*"})]}),h.jsx(ko,{type:"text",value:c,onChange:F=>f(F.target.value),required:!0})]}),h.jsxs(Vi,{children:[h.jsxs(Wi,{children:["비밀번호 ",h.jsx(Pu,{children:"*"})]}),h.jsx(ko,{type:"password",value:p,onChange:F=>g(F.target.value),required:!0})]}),h.jsxs(Vi,{children:[h.jsx(Wi,{children:"프로필 이미지"}),h.jsxs(l0,{children:[h.jsx(u0,{src:S||zt,alt:"profile"}),h.jsx(a0,{type:"file",accept:"image/*",onChange:C,id:"profile-image"}),h.jsx(c0,{htmlFor:"profile-image",children:"이미지 변경"})]})]}),R&&h.jsx(rh,{children:R}),h.jsx(nh,{type:"submit",children:"계속하기"}),h.jsx(d0,{onClick:i,children:"이미 계정이 있으신가요?"})]})]})}):null},h0=({isOpen:r,onClose:i})=>{const[s,l]=ie.useState(""),[c,f]=ie.useState(""),[p,g]=ie.useState(""),[x,v]=ie.useState(!1),S=vt(I=>I.setCurrentUser),{fetchUsers:A}=nn(),R=async()=>{var I;try{const _=await Gv(s,c);await A(),S(_),g(""),i()}catch(_){console.error("로그인 에러:",_),((I=_.response)==null?void 0:I.status)===401?g("아이디 또는 비밀번호가 올바르지 않습니다."):g("로그인에 실패했습니다.")}};return r?h.jsxs(h.Fragment,{children:[h.jsx(eh,{children:h.jsxs(th,{children:[h.jsx("h2",{children:"돌아오신 것을 환영해요!"}),h.jsxs("form",{onSubmit:I=>{I.preventDefault(),R()},children:[h.jsx(ko,{type:"text",placeholder:"사용자 이름",value:s,onChange:I=>l(I.target.value)}),h.jsx(ko,{type:"password",placeholder:"비밀번호",value:c,onChange:I=>f(I.target.value)}),p&&h.jsx(rh,{children:p}),h.jsx(nh,{type:"submit",children:"로그인"})]}),h.jsxs(i0,{children:["계정이 필요한가요? ",h.jsx(s0,{onClick:()=>v(!0),children:"가입하기"})]})]})}),h.jsx(p0,{isOpen:x,onClose:()=>v(!1)})]}):null},m0=async r=>(await Je.get(`/channels?userId=${r}`)).data,g0=async r=>(await Je.post("/channels/public",r)).data,y0=async r=>{const i={participantIds:r};return(await Je.post("/channels/private",i)).data},v0=async r=>(await Je.get("/readStatuses",{params:{userId:r}})).data,w0=async(r,i)=>{const s={newLastReadAt:i};return(await Je.patch(`/readStatuses/${r}`,s)).data},x0=async(r,i,s)=>{const l={userId:r,channelId:i,lastReadAt:s};return(await Je.post("/readStatuses",l)).data},jo=_r((r,i)=>({readStatuses:{},fetchReadStatuses:async()=>{try{const s=vt.getState().currentUserId;if(!s)return;const c=(await v0(s)).reduce((f,p)=>(f[p.channelId]={id:p.id,lastReadAt:p.lastReadAt},f),{});r({readStatuses:c})}catch(s){console.error("읽음 상태 조회 실패:",s)}},updateReadStatus:async s=>{try{const l=vt.getState().currentUserId;if(!l)return;const c=i().readStatuses[s];let f;c?f=await w0(c.id,new Date().toISOString()):f=await x0(l,s,new Date().toISOString()),r(p=>({readStatuses:{...p.readStatuses,[s]:{id:f.id,lastReadAt:f.lastReadAt}}}))}catch(l){console.error("읽음 상태 업데이트 실패:",l)}},hasUnreadMessages:(s,l)=>{const c=i().readStatuses[s],f=c==null?void 0:c.lastReadAt;return!f||new Date(l)>new Date(f)}})),xr=_r((r,i)=>({channels:[],pollingInterval:null,loading:!1,error:null,fetchChannels:async s=>{r({loading:!0,error:null});try{const l=await m0(s);r(f=>{const p=new Set(f.channels.map(S=>S.id)),g=l.filter(S=>!p.has(S.id));return{channels:[...f.channels.filter(S=>l.some(A=>A.id===S.id)),...g],loading:!1}});const{fetchReadStatuses:c}=jo.getState();return c(),l}catch(l){return r({error:l,loading:!1}),[]}},startPolling:s=>{const l=i().pollingInterval;l&&clearInterval(l);const c=setInterval(()=>{i().fetchChannels(s)},3e3);r({pollingInterval:c})},stopPolling:()=>{const s=i().pollingInterval;s&&(clearInterval(s),r({pollingInterval:null}))},createPublicChannel:async s=>{try{const l=await g0(s);return r(c=>c.channels.some(p=>p.id===l.id)?c:{channels:[...c.channels,{...l,participantIds:[],lastMessageAt:new Date().toISOString()}]}),l}catch(l){throw console.error("공개 채널 생성 실패:",l),l}},createPrivateChannel:async s=>{try{const l=await y0(s);return r(c=>c.channels.some(p=>p.id===l.id)?c:{channels:[...c.channels,{...l,participantIds:s,lastMessageAt:new Date().toISOString()}]}),l}catch(l){throw console.error("비공개 채널 생성 실패:",l),l}}})),S0=async r=>(await Je.get(`/binaryContents/${r}`)).data,E0=r=>`${Qv()}/binaryContents/${r}/download`,Yn=_r((r,i)=>({binaryContents:{},fetchBinaryContent:async s=>{if(i().binaryContents[s])return i().binaryContents[s];try{const l=await S0(s),{contentType:c,fileName:f,size:p}=l,x={url:E0(s),contentType:c,fileName:f,size:p};return r(v=>({binaryContents:{...v.binaryContents,[s]:x}})),x}catch(l){return console.error("첨부파일 정보 조회 실패:",l),null}}})),Io=T.div` + position: absolute; + bottom: -3px; + right: -3px; + width: 16px; + height: 16px; + border-radius: 50%; + background: ${r=>r.$online?ee.colors.status.online:ee.colors.status.offline}; + border: 4px solid ${r=>r.$background||ee.colors.background.secondary}; +`;T.div` + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 8px; + background: ${r=>ee.colors.status[r.status||"offline"]||ee.colors.status.offline}; +`;const Tr=T.div` + position: relative; + width: ${r=>r.$size||"32px"}; + height: ${r=>r.$size||"32px"}; + flex-shrink: 0; + margin: ${r=>r.$margin||"0"}; +`,rn=T.img` + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + border: ${r=>r.$border||"none"}; +`;function C0({isOpen:r,onClose:i,user:s}){var L,b;const[l,c]=ie.useState(s.username),[f,p]=ie.useState(s.email),[g,x]=ie.useState(""),[v,S]=ie.useState(null),[A,R]=ie.useState(""),[I,_]=ie.useState(null),{binaryContents:C,fetchBinaryContent:O}=Yn(),{logout:F,updateUser:B}=vt();ie.useEffect(()=>{var re;(re=s.profile)!=null&&re.id&&!C[s.profile.id]&&O(s.profile.id)},[s.profile,C,O]);const V=()=>{c(s.username),p(s.email),x(""),S(null),_(null),R(""),i()},Q=re=>{var Ne;const ye=(Ne=re.target.files)==null?void 0:Ne[0];if(ye){S(ye);const at=new FileReader;at.onloadend=()=>{_(at.result)},at.readAsDataURL(ye)}},H=async re=>{re.preventDefault(),R("");try{const ye=new FormData,Ne={};l!==s.username&&(Ne.newUsername=l),f!==s.email&&(Ne.newEmail=f),g&&(Ne.newPassword=g),(Object.keys(Ne).length>0||v)&&(ye.append("userUpdateRequest",new Blob([JSON.stringify(Ne)],{type:"application/json"})),v&&ye.append("profile",v),await B(s.id,ye)),i()}catch{R("사용자 정보 수정에 실패했습니다.")}};return r?h.jsx(k0,{children:h.jsxs(j0,{children:[h.jsx("h2",{children:"프로필 수정"}),h.jsxs("form",{onSubmit:H,children:[h.jsxs(Yi,{children:[h.jsx(qi,{children:"프로필 이미지"}),h.jsxs(R0,{children:[h.jsx(P0,{src:I||((L=s.profile)!=null&&L.id?(b=C[s.profile.id])==null?void 0:b.url:void 0)||zt,alt:"profile"}),h.jsx(_0,{type:"file",accept:"image/*",onChange:Q,id:"profile-image"}),h.jsx(T0,{htmlFor:"profile-image",children:"이미지 변경"})]})]}),h.jsxs(Yi,{children:[h.jsxs(qi,{children:["사용자명 ",h.jsx(bd,{children:"*"})]}),h.jsx(_u,{type:"text",value:l,onChange:re=>c(re.target.value),required:!0})]}),h.jsxs(Yi,{children:[h.jsxs(qi,{children:["이메일 ",h.jsx(bd,{children:"*"})]}),h.jsx(_u,{type:"email",value:f,onChange:re=>p(re.target.value),required:!0})]}),h.jsxs(Yi,{children:[h.jsx(qi,{children:"새 비밀번호"}),h.jsx(_u,{type:"password",placeholder:"변경하지 않으려면 비워두세요",value:g,onChange:re=>x(re.target.value)})]}),A&&h.jsx(A0,{children:A}),h.jsxs(I0,{children:[h.jsx(Hd,{type:"button",onClick:V,$secondary:!0,children:"취소"}),h.jsx(Hd,{type:"submit",children:"저장"})]})]}),h.jsx(N0,{onClick:F,children:"로그아웃"})]})}):null}const k0=T.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`,j0=T.div` + background: ${({theme:r})=>r.colors.background.secondary}; + padding: 32px; + border-radius: 5px; + width: 100%; + max-width: 480px; + + h2 { + color: ${({theme:r})=>r.colors.text.primary}; + margin-bottom: 24px; + text-align: center; + font-size: 24px; + } +`,_u=T.input` + width: 100%; + padding: 10px; + margin-bottom: 10px; + border: none; + border-radius: 4px; + background: ${({theme:r})=>r.colors.background.input}; + color: ${({theme:r})=>r.colors.text.primary}; + + &::placeholder { + color: ${({theme:r})=>r.colors.text.muted}; + } + + &:focus { + outline: none; + box-shadow: 0 0 0 2px ${({theme:r})=>r.colors.brand.primary}; + } +`,Hd=T.button` + width: 100%; + padding: 10px; + border: none; + border-radius: 4px; + background: ${({$secondary:r,theme:i})=>r?"transparent":i.colors.brand.primary}; + color: ${({theme:r})=>r.colors.text.primary}; + cursor: pointer; + font-weight: 500; + + &:hover { + background: ${({$secondary:r,theme:i})=>r?i.colors.background.hover:i.colors.brand.hover}; + } +`,A0=T.div` + color: ${({theme:r})=>r.colors.status.error}; + font-size: 14px; + margin-bottom: 10px; +`,R0=T.div` + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; +`,P0=T.img` + width: 100px; + height: 100px; + border-radius: 50%; + margin-bottom: 10px; + object-fit: cover; +`,_0=T.input` + display: none; +`,T0=T.label` + color: ${({theme:r})=>r.colors.brand.primary}; + cursor: pointer; + font-size: 14px; + + &:hover { + text-decoration: underline; + } +`,I0=T.div` + display: flex; + gap: 10px; + margin-top: 20px; +`,N0=T.button` + width: 100%; + padding: 10px; + margin-top: 16px; + border: none; + border-radius: 4px; + background: transparent; + color: ${({theme:r})=>r.colors.status.error}; + cursor: pointer; + font-weight: 500; + + &:hover { + background: ${({theme:r})=>r.colors.status.error}20; + } +`,Yi=T.div` + margin-bottom: 20px; +`,qi=T.label` + display: block; + color: ${({theme:r})=>r.colors.text.muted}; + font-size: 12px; + font-weight: 700; + margin-bottom: 8px; +`,bd=T.span` + color: ${({theme:r})=>r.colors.status.error}; +`,O0=T.div` + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + background-color: ${({theme:r})=>r.colors.background.tertiary}; + width: 100%; + height: 52px; +`,L0=T(Tr)``;T(rn)``;const D0=T.div` + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; +`,M0=T.div` + font-weight: 500; + color: ${({theme:r})=>r.colors.text.primary}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.875rem; + line-height: 1.2; +`,z0=T.div` + font-size: 0.75rem; + color: ${({theme:r})=>r.colors.text.secondary}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.2; +`,U0=T.div` + display: flex; + align-items: center; + flex-shrink: 0; +`,F0=T.button` + background: none; + border: none; + padding: 0.25rem; + cursor: pointer; + color: ${({theme:r})=>r.colors.text.secondary}; + font-size: 18px; + + &:hover { + color: ${({theme:r})=>r.colors.text.primary}; + } +`;function B0({user:r}){var f,p;const[i,s]=ie.useState(!1),{binaryContents:l,fetchBinaryContent:c}=Yn();return ie.useEffect(()=>{var g;(g=r.profile)!=null&&g.id&&!l[r.profile.id]&&c(r.profile.id)},[r.profile,l,c]),h.jsxs(h.Fragment,{children:[h.jsxs(O0,{children:[h.jsxs(L0,{children:[h.jsx(rn,{src:(f=r.profile)!=null&&f.id?(p=l[r.profile.id])==null?void 0:p.url:zt,alt:r.username}),h.jsx(Io,{$online:!0})]}),h.jsxs(D0,{children:[h.jsx(M0,{children:r.username}),h.jsx(z0,{children:"온라인"})]}),h.jsx(U0,{children:h.jsx(F0,{onClick:()=>s(!0),children:"⚙️"})})]}),h.jsx(C0,{isOpen:i,onClose:()=>s(!1),user:r})]})}const $0=T.div` + width: 240px; + background: ${ee.colors.background.secondary}; + border-right: 1px solid ${ee.colors.border.primary}; + display: flex; + flex-direction: column; +`,H0=T.div` + flex: 1; + overflow-y: auto; +`,b0=T.div` + padding: 16px; + font-size: 16px; + font-weight: bold; + color: ${ee.colors.text.primary}; +`,oh=T.div` + height: 34px; + padding: 0 8px; + margin: 1px 8px; + display: flex; + align-items: center; + gap: 6px; + color: ${r=>r.$hasUnread?r.theme.colors.text.primary:r.theme.colors.text.muted}; + font-weight: ${r=>r.$hasUnread?"600":"normal"}; + cursor: pointer; + background: ${r=>r.$isActive?r.theme.colors.background.hover:"transparent"}; + border-radius: 4px; + + &:hover { + background: ${r=>r.theme.colors.background.hover}; + color: ${r=>r.theme.colors.text.primary}; + } +`,Vd=T.div` + margin-bottom: 8px; +`,qu=T.div` + padding: 8px 16px; + display: flex; + align-items: center; + color: ${ee.colors.text.muted}; + text-transform: uppercase; + font-size: 12px; + font-weight: 600; + cursor: pointer; + user-select: none; + + & > span:nth-child(2) { + flex: 1; + margin-right: auto; + } + + &:hover { + color: ${ee.colors.text.primary}; + } +`,Wd=T.span` + margin-right: 4px; + font-size: 10px; + transition: transform 0.2s; + transform: rotate(${r=>r.$folded?"-90deg":"0deg"}); +`,Yd=T.div` + display: ${r=>r.$folded?"none":"block"}; +`,qd=T(oh)` + height: ${r=>r.hasSubtext?"42px":"34px"}; +`,V0=T(Tr)` + width: 32px; + height: 32px; + margin: 0 8px; +`,Qd=T.div` + font-size: 16px; + line-height: 18px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: ${r=>r.$isActive||r.$hasUnread?r.theme.colors.text.primary:r.theme.colors.text.muted}; + font-weight: ${r=>r.$hasUnread?"600":"normal"}; +`;T(Io)` + border-color: ${ee.colors.background.primary}; +`;const Gd=T.button` + background: none; + border: none; + color: ${ee.colors.text.muted}; + font-size: 18px; + padding: 0; + cursor: pointer; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s, color 0.2s; + + ${qu}:hover & { + opacity: 1; + } + + &:hover { + color: ${ee.colors.text.primary}; + } +`,W0=T(Tr)` + width: 40px; + height: 24px; + margin: 0 8px; +`,Y0=T.div` + font-size: 12px; + line-height: 13px; + color: ${ee.colors.text.muted}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`,Kd=T.div` + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; +`,q0=T.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`,Q0=T.div` + background: ${ee.colors.background.primary}; + border-radius: 4px; + width: 440px; + max-width: 90%; +`,G0=T.div` + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; +`,K0=T.h2` + color: ${ee.colors.text.primary}; + font-size: 20px; + font-weight: 600; + margin: 0; +`,X0=T.div` + padding: 0 16px 16px; +`,J0=T.form` + display: flex; + flex-direction: column; + gap: 16px; +`,Tu=T.div` + display: flex; + flex-direction: column; + gap: 8px; +`,Iu=T.label` + color: ${ee.colors.text.primary}; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +`,Z0=T.p` + color: ${ee.colors.text.muted}; + font-size: 14px; + margin: -4px 0 0; +`,Qu=T.input` + padding: 10px; + background: ${ee.colors.background.tertiary}; + border: none; + border-radius: 3px; + color: ${ee.colors.text.primary}; + font-size: 16px; + + &:focus { + outline: none; + box-shadow: 0 0 0 2px ${ee.colors.status.online}; + } + + &::placeholder { + color: ${ee.colors.text.muted}; + } +`,e1=T.button` + margin-top: 8px; + padding: 12px; + background: ${ee.colors.status.online}; + color: white; + border: none; + border-radius: 3px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; + + &:hover { + background: #3ca374; + } +`,t1=T.button` + background: none; + border: none; + color: ${ee.colors.text.muted}; + font-size: 24px; + cursor: pointer; + padding: 4px; + line-height: 1; + + &:hover { + color: ${ee.colors.text.primary}; + } +`,n1=T(Qu)` + margin-bottom: 8px; +`,r1=T.div` + max-height: 300px; + overflow-y: auto; + background: ${ee.colors.background.tertiary}; + border-radius: 4px; +`,o1=T.div` + display: flex; + align-items: center; + padding: 8px 12px; + cursor: pointer; + transition: background 0.2s; + + &:hover { + background: ${ee.colors.background.hover}; + } + + & + & { + border-top: 1px solid ${ee.colors.border.primary}; + } +`,i1=T.input` + margin-right: 12px; + width: 16px; + height: 16px; + cursor: pointer; +`,Xd=T.img` + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: 12px; +`,s1=T.div` + flex: 1; + min-width: 0; +`,l1=T.div` + color: ${ee.colors.text.primary}; + font-size: 14px; + font-weight: 500; +`,u1=T.div` + color: ${ee.colors.text.muted}; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`,a1=T.div` + padding: 16px; + text-align: center; + color: ${ee.colors.text.muted}; +`,c1=T.div` + color: ${ee.colors.status.error}; + font-size: 14px; + padding: 8px 0; + text-align: center; + background-color: ${({theme:r})=>r.colors.background.tertiary}; + border-radius: 4px; + margin-bottom: 8px; +`;function f1(){return h.jsx(b0,{children:"채널 목록"})}function Jd({channel:r,isActive:i,onClick:s,hasUnread:l}){var x;const c=vt(v=>v.currentUserId),{binaryContents:f}=Yn();if(r.type==="PUBLIC")return h.jsxs(oh,{$isActive:i,onClick:s,$hasUnread:l,children:["# ",r.name]});const p=r.participants;if(p.length>2){const v=p.filter(S=>S.id!==c).map(S=>S.username).join(", ");return h.jsxs(qd,{$isActive:i,onClick:s,children:[h.jsx(W0,{children:p.filter(S=>S.id!==c).slice(0,2).map((S,A)=>{var R;return h.jsx(rn,{src:S.profile?(R=f[S.profile.id])==null?void 0:R.url:zt,style:{position:"absolute",left:A*16,zIndex:2-A,width:"24px",height:"24px",border:"2px solid #2a2a2a"}},S.id)})}),h.jsxs(Kd,{children:[h.jsx(Qd,{$hasUnread:l,children:v}),h.jsxs(Y0,{children:["멤버 ",p.length,"명"]})]})]})}const g=p.filter(v=>v.id!==c)[0];return g&&h.jsxs(qd,{$isActive:i,onClick:s,children:[h.jsxs(V0,{children:[h.jsx(rn,{src:g.profile?(x=f[g.profile.id])==null?void 0:x.url:zt,alt:"profile"}),h.jsx(Io,{$online:g.online})]}),h.jsx(Kd,{children:h.jsx(Qd,{$hasUnread:l,children:g.username})})]})}function d1({isOpen:r,type:i,onClose:s,onCreateSuccess:l}){const[c,f]=ie.useState({name:"",description:""}),[p,g]=ie.useState(""),[x,v]=ie.useState([]),[S,A]=ie.useState(""),R=nn(H=>H.users),I=Yn(H=>H.binaryContents),_=vt(H=>H.currentUserId),C=ie.useMemo(()=>R.filter(H=>H.id!==_).filter(H=>H.username.toLowerCase().includes(p.toLowerCase())||H.email.toLowerCase().includes(p.toLowerCase())),[p,R,_]),O=xr(H=>H.createPublicChannel),F=xr(H=>H.createPrivateChannel),B=H=>{const{name:L,value:b}=H.target;f(re=>({...re,[L]:b}))},V=H=>{v(L=>L.includes(H)?L.filter(b=>b!==H):[...L,H])},Q=async H=>{var L,b;H.preventDefault(),A("");try{let re;if(i==="PUBLIC"){if(!c.name.trim()){A("채널 이름을 입력해주세요.");return}const ye={name:c.name,description:c.description};re=await O(ye)}else{if(x.length===0){A("대화 상대를 선택해주세요.");return}const ye=_&&[...x,_]||x;re=await F(ye)}l(re)}catch(re){console.error("채널 생성 실패:",re),A(((b=(L=re.response)==null?void 0:L.data)==null?void 0:b.message)||"채널 생성에 실패했습니다. 다시 시도해주세요.")}};return r?h.jsx(q0,{onClick:s,children:h.jsxs(Q0,{onClick:H=>H.stopPropagation(),children:[h.jsxs(G0,{children:[h.jsx(K0,{children:i==="PUBLIC"?"채널 만들기":"개인 메시지 시작하기"}),h.jsx(t1,{onClick:s,children:"×"})]}),h.jsx(X0,{children:h.jsxs(J0,{onSubmit:Q,children:[S&&h.jsx(c1,{children:S}),i==="PUBLIC"?h.jsxs(h.Fragment,{children:[h.jsxs(Tu,{children:[h.jsx(Iu,{children:"채널 이름"}),h.jsx(Qu,{name:"name",value:c.name,onChange:B,placeholder:"새로운-채널",required:!0})]}),h.jsxs(Tu,{children:[h.jsx(Iu,{children:"채널 설명"}),h.jsx(Z0,{children:"이 채널의 주제를 설명해주세요."}),h.jsx(Qu,{name:"description",value:c.description,onChange:B,placeholder:"채널 설명을 입력하세요"})]})]}):h.jsxs(Tu,{children:[h.jsx(Iu,{children:"사용자 검색"}),h.jsx(n1,{type:"text",value:p,onChange:H=>g(H.target.value),placeholder:"사용자명 또는 이메일로 검색"}),h.jsx(r1,{children:C.length>0?C.map(H=>h.jsxs(o1,{children:[h.jsx(i1,{type:"checkbox",checked:x.includes(H.id),onChange:()=>V(H.id)}),H.profile?h.jsx(Xd,{src:I[H.profile.id].url}):h.jsx(Xd,{src:zt}),h.jsxs(s1,{children:[h.jsx(l1,{children:H.username}),h.jsx(u1,{children:H.email})]})]},H.id)):h.jsx(a1,{children:"검색 결과가 없습니다."})})]}),h.jsx(e1,{type:"submit",children:i==="PUBLIC"?"채널 만들기":"대화 시작하기"})]})})]})}):null}function p1({currentUser:r,activeChannel:i,onChannelSelect:s}){var Q,H;const[l,c]=ie.useState({PUBLIC:!1,PRIVATE:!1}),[f,p]=ie.useState({isOpen:!1,type:null}),g=xr(L=>L.channels),x=xr(L=>L.fetchChannels),v=xr(L=>L.startPolling),S=xr(L=>L.stopPolling),A=jo(L=>L.fetchReadStatuses),R=jo(L=>L.updateReadStatus),I=jo(L=>L.hasUnreadMessages);ie.useEffect(()=>{if(r)return x(r.id),A(),v(r.id),()=>{S()}},[r,x,A,v,S]);const _=L=>{c(b=>({...b,[L]:!b[L]}))},C=(L,b)=>{b.stopPropagation(),p({isOpen:!0,type:L})},O=()=>{p({isOpen:!1,type:null})},F=async L=>{try{const re=(await x(r.id)).find(ye=>ye.id===L.id);re&&s(re),O()}catch(b){console.error("채널 생성 실패:",b)}},B=L=>{s(L),R(L.id)},V=g.reduce((L,b)=>(L[b.type]||(L[b.type]=[]),L[b.type].push(b),L),{});return h.jsxs($0,{children:[h.jsx(f1,{}),h.jsxs(H0,{children:[h.jsxs(Vd,{children:[h.jsxs(qu,{onClick:()=>_("PUBLIC"),children:[h.jsx(Wd,{$folded:l.PUBLIC,children:"▼"}),h.jsx("span",{children:"일반 채널"}),h.jsx(Gd,{onClick:L=>C("PUBLIC",L),children:"+"})]}),h.jsx(Yd,{$folded:l.PUBLIC,children:(Q=V.PUBLIC)==null?void 0:Q.map(L=>h.jsx(Jd,{channel:L,isActive:(i==null?void 0:i.id)===L.id,hasUnread:I(L.id,L.lastMessageAt),onClick:()=>B(L)},L.id))})]}),h.jsxs(Vd,{children:[h.jsxs(qu,{onClick:()=>_("PRIVATE"),children:[h.jsx(Wd,{$folded:l.PRIVATE,children:"▼"}),h.jsx("span",{children:"개인 메시지"}),h.jsx(Gd,{onClick:L=>C("PRIVATE",L),children:"+"})]}),h.jsx(Yd,{$folded:l.PRIVATE,children:(H=V.PRIVATE)==null?void 0:H.map(L=>h.jsx(Jd,{channel:L,isActive:(i==null?void 0:i.id)===L.id,hasUnread:I(L.id,L.lastMessageAt),onClick:()=>B(L)},L.id))})]})]}),h.jsx(h1,{children:h.jsx(B0,{user:r})}),h.jsx(d1,{isOpen:f.isOpen,type:f.type,onClose:O,onCreateSuccess:F})]})}const h1=T.div` + margin-top: auto; + border-top: 1px solid ${({theme:r})=>r.colors.border.primary}; + background-color: ${({theme:r})=>r.colors.background.tertiary}; +`,m1=T.div` + flex: 1; + display: flex; + flex-direction: column; + background: ${({theme:r})=>r.colors.background.primary}; +`,g1=T.div` + display: flex; + flex-direction: column; + height: 100%; + background: ${({theme:r})=>r.colors.background.primary}; +`,y1=T(g1)` + justify-content: center; + align-items: center; + flex: 1; + padding: 0 20px; +`,v1=T.div` + text-align: center; + max-width: 400px; + padding: 20px; + margin-bottom: 80px; +`,w1=T.div` + font-size: 48px; + margin-bottom: 16px; + animation: wave 2s infinite; + transform-origin: 70% 70%; + + @keyframes wave { + 0% { transform: rotate(0deg); } + 10% { transform: rotate(14deg); } + 20% { transform: rotate(-8deg); } + 30% { transform: rotate(14deg); } + 40% { transform: rotate(-4deg); } + 50% { transform: rotate(10deg); } + 60% { transform: rotate(0deg); } + 100% { transform: rotate(0deg); } + } +`,x1=T.h2` + color: ${({theme:r})=>r.colors.text.primary}; + font-size: 28px; + font-weight: 700; + margin-bottom: 16px; +`,S1=T.p` + color: ${({theme:r})=>r.colors.text.muted}; + font-size: 16px; + line-height: 1.6; + word-break: keep-all; +`,Zd=T.div` + height: 48px; + padding: 0 16px; + background: ${ee.colors.background.primary}; + border-bottom: 1px solid ${ee.colors.border.primary}; + display: flex; + align-items: center; +`,ep=T.div` + display: flex; + align-items: center; + gap: 8px; + height: 100%; +`,E1=T.div` + display: flex; + align-items: center; + gap: 12px; + height: 100%; +`,C1=T(Tr)` + width: 24px; + height: 24px; +`;T.img` + width: 24px; + height: 24px; + border-radius: 50%; +`;const k1=T.div` + position: relative; + width: 40px; + height: 24px; + flex-shrink: 0; +`,j1=T(Io)` + border-color: ${ee.colors.background.primary}; + bottom: -3px; + right: -3px; +`,A1=T.div` + font-size: 12px; + color: ${ee.colors.text.muted}; + line-height: 13px; +`,tp=T.div` + font-weight: bold; + color: ${ee.colors.text.primary}; + line-height: 20px; + font-size: 16px; +`,R1=T.div` + flex: 1; + display: flex; + flex-direction: column-reverse; + overflow-y: auto; +`,P1=T.div` + padding: 16px; + display: flex; + flex-direction: column; +`,_1=T.div` + margin-bottom: 16px; + display: flex; + align-items: flex-start; +`,T1=T(Tr)` + margin-right: 16px; + width: 40px; + height: 40px; +`;T.img` + width: 40px; + height: 40px; + border-radius: 50%; +`;const I1=T.div` + display: flex; + align-items: center; + margin-bottom: 4px; +`,N1=T.span` + font-weight: bold; + color: ${ee.colors.text.primary}; + margin-right: 8px; +`,O1=T.span` + font-size: 0.75rem; + color: ${ee.colors.text.muted}; +`,L1=T.div` + color: ${ee.colors.text.secondary}; + margin-top: 4px; +`,D1=T.form` + display: flex; + align-items: center; + gap: 8px; + padding: 16px; + background: ${({theme:r})=>r.colors.background.secondary}; +`,M1=T.textarea` + flex: 1; + padding: 12px; + background: ${({theme:r})=>r.colors.background.tertiary}; + border: none; + border-radius: 4px; + color: ${({theme:r})=>r.colors.text.primary}; + font-size: 14px; + resize: none; + min-height: 44px; + max-height: 144px; + + &:focus { + outline: none; + } + + &::placeholder { + color: ${({theme:r})=>r.colors.text.muted}; + } +`,z1=T.button` + background: none; + border: none; + color: ${({theme:r})=>r.colors.text.muted}; + font-size: 24px; + cursor: pointer; + padding: 4px 8px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + color: ${({theme:r})=>r.colors.text.primary}; + } +`;T.div` + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: ${ee.colors.text.muted}; + font-size: 16px; + font-weight: 500; + padding: 20px; + text-align: center; +`;const np=T.div` + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; + width: 100%; +`,U1=T.a` + display: block; + border-radius: 4px; + overflow: hidden; + max-width: 300px; + + img { + width: 100%; + height: auto; + display: block; + } +`,F1=T.a` + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: ${({theme:r})=>r.colors.background.tertiary}; + border-radius: 8px; + text-decoration: none; + width: fit-content; + + &:hover { + background: ${({theme:r})=>r.colors.background.hover}; + } +`,B1=T.div` + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; + color: #0B93F6; +`,$1=T.div` + display: flex; + flex-direction: column; + gap: 2px; +`,H1=T.span` + font-size: 14px; + color: #0B93F6; + font-weight: 500; +`,b1=T.span` + font-size: 13px; + color: ${({theme:r})=>r.colors.text.muted}; +`,V1=T.div` + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 8px 0; +`,ih=T.div` + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: ${({theme:r})=>r.colors.background.tertiary}; + border-radius: 4px; + max-width: 300px; +`,W1=T(ih)` + padding: 0; + overflow: hidden; + width: 200px; + height: 120px; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +`,Y1=T.div` + color: #0B93F6; + font-size: 20px; +`,q1=T.div` + font-size: 13px; + color: ${({theme:r})=>r.colors.text.primary}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`,rp=T.button` + position: absolute; + top: -6px; + right: -6px; + width: 20px; + height: 20px; + border-radius: 50%; + background: ${({theme:r})=>r.colors.background.secondary}; + border: none; + color: ${({theme:r})=>r.colors.text.muted}; + font-size: 16px; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + + &:hover { + color: ${({theme:r})=>r.colors.text.primary}; + } +`;function Q1({channel:r}){var x;const i=vt(v=>v.currentUserId),s=nn(v=>v.users),l=Yn(v=>v.binaryContents);if(!r)return null;if(r.type==="PUBLIC")return h.jsx(Zd,{children:h.jsx(ep,{children:h.jsxs(tp,{children:["# ",r.name]})})});const c=r.participants.map(v=>s.find(S=>S.id===v.id)).filter(Boolean),f=c.filter(v=>v.id!==i),p=c.length>2,g=c.filter(v=>v.id!==i).map(v=>v.username).join(", ");return h.jsx(Zd,{children:h.jsx(ep,{children:h.jsxs(E1,{children:[p?h.jsx(k1,{children:f.slice(0,2).map((v,S)=>{var A;return h.jsx(rn,{src:v.profile?(A=l[v.profile.id])==null?void 0:A.url:zt,style:{position:"absolute",left:S*16,zIndex:2-S,width:"24px",height:"24px"}},v.id)})}):h.jsxs(C1,{children:[h.jsx(rn,{src:f[0].profile?(x=l[f[0].profile.id])==null?void 0:x.url:zt}),h.jsx(j1,{$online:f[0].online})]}),h.jsxs("div",{children:[h.jsx(tp,{children:g}),p&&h.jsxs(A1,{children:["멤버 ",c.length,"명"]})]})]})})})}const G1=async(r,i)=>{var l;return(await Je.get("/messages",{params:{channelId:r,page:i==null?void 0:i.page,size:i==null?void 0:i.size,sort:(l=i==null?void 0:i.sort)==null?void 0:l.join(",")}})).data},K1=async(r,i)=>{const s=new FormData,l={content:r.content,channelId:r.channelId,authorId:r.authorId};return s.append("messageCreateRequest",new Blob([JSON.stringify(l)],{type:"application/json"})),i&&i.length>0&&i.forEach(f=>{s.append("attachments",f)}),(await Je.post("/messages",s,{headers:{"Content-Type":"multipart/form-data"}})).data},Nu={page:0,size:50,sort:["createdAt,desc"]},sh=_r((r,i)=>({messages:[],pollingIntervals:{},lastMessageId:null,pagination:{currentPage:0,pageSize:50,hasNext:!1},fetchMessages:async(s,l=Nu)=>{try{const c=await G1(s,l),f=c.content,p=f.length>0?f[0]:null,g=(p==null?void 0:p.id)!==i().lastMessageId;return r(x=>{var _;const v=l.page===0,S=s!==((_=x.messages[0])==null?void 0:_.channelId),A=v&&(x.messages.length===0||S);let R=[],I={...x.pagination};if(A)R=f,I={currentPage:c.number,pageSize:c.size,hasNext:c.hasNext};else if(v){const C=new Set(x.messages.map(F=>F.id));R=[...f.filter(F=>!C.has(F.id)&&(x.messages.length===0||F.createdAt>x.messages[0].createdAt)),...x.messages]}else{if(x.messages.length>0){const C=new Set(x.messages.map(F=>F.id)),O=f.filter(F=>!C.has(F.id)&&F.createdAt{const{pagination:l}=i();if(!l.hasNext)return;const c={...Nu,page:l.currentPage+1};await i().fetchMessages(s,c)},startPolling:s=>{const l=i();if(l.pollingIntervals[s]){const g=l.pollingIntervals[s];typeof g=="number"&&clearTimeout(g)}let c=300;const f=3e3;r(g=>({pollingIntervals:{...g.pollingIntervals,[s]:!0}}));const p=async()=>{const g=i();if(!g.pollingIntervals[s])return;if(await g.fetchMessages(s,Nu)?c=300:c=Math.min(c*1.5,f),i().pollingIntervals[s]){const v=setTimeout(p,c);r(S=>({pollingIntervals:{...S.pollingIntervals,[s]:v}}))}};p()},stopPolling:s=>{const{pollingIntervals:l}=i();if(l[s]){const c=l[s];typeof c=="number"&&clearTimeout(c),r(f=>{const p={...f.pollingIntervals};return delete p[s],{pollingIntervals:p}})}},createMessage:async(s,l)=>{try{const c=await K1(s,l),f=jo.getState().updateReadStatus;return await f(s.channelId),r(p=>p.messages.some(x=>x.id===c.id)?p:{messages:[c,...p.messages],lastMessageId:c.id}),c}catch(c){throw console.error("메시지 생성 실패:",c),c}}}));function X1({channel:r}){const[i,s]=ie.useState(""),[l,c]=ie.useState([]),f=sh(R=>R.createMessage),p=vt(R=>R.currentUserId),g=async R=>{if(R.preventDefault(),!(!i.trim()&&l.length===0))try{await f({content:i.trim(),channelId:r.id,authorId:p??""},l),s(""),c([])}catch(I){console.error("메시지 전송 실패:",I)}},x=R=>{const I=Array.from(R.target.files||[]);c(_=>[..._,...I]),R.target.value=""},v=R=>{c(I=>I.filter((_,C)=>C!==R))},S=R=>{if(R.key==="Enter"&&!R.shiftKey){if(console.log("Enter key pressed"),R.preventDefault(),R.nativeEvent.isComposing)return;g(R)}},A=(R,I)=>R.type.startsWith("image/")?h.jsxs(W1,{children:[h.jsx("img",{src:URL.createObjectURL(R),alt:R.name}),h.jsx(rp,{onClick:()=>v(I),children:"×"})]},I):h.jsxs(ih,{children:[h.jsx(Y1,{children:"📎"}),h.jsx(q1,{children:R.name}),h.jsx(rp,{onClick:()=>v(I),children:"×"})]},I);return ie.useEffect(()=>()=>{l.forEach(R=>{R.type.startsWith("image/")&&URL.revokeObjectURL(URL.createObjectURL(R))})},[l]),r?h.jsxs(h.Fragment,{children:[l.length>0&&h.jsx(V1,{children:l.map((R,I)=>A(R,I))}),h.jsxs(D1,{onSubmit:g,children:[h.jsxs(z1,{as:"label",children:["+",h.jsx("input",{type:"file",multiple:!0,onChange:x,style:{display:"none"}})]}),h.jsx(M1,{value:i,onChange:R=>s(R.target.value),onKeyDown:S,placeholder:r.type==="PUBLIC"?`#${r.name}에 메시지 보내기`:"메시지 보내기"})]})]}):null}/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */var Gu=function(r,i){return Gu=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,l){s.__proto__=l}||function(s,l){for(var c in l)l.hasOwnProperty(c)&&(s[c]=l[c])},Gu(r,i)};function J1(r,i){Gu(r,i);function s(){this.constructor=r}r.prototype=i===null?Object.create(i):(s.prototype=i.prototype,new s)}var Ao=function(){return Ao=Object.assign||function(i){for(var s,l=1,c=arguments.length;lr?I():i!==!0&&(c=setTimeout(l?_:I,l===void 0?r-A:r))}return v.cancel=x,v}var Sr={Pixel:"Pixel",Percent:"Percent"},op={unit:Sr.Percent,value:.8};function ip(r){return typeof r=="number"?{unit:Sr.Percent,value:r*100}:typeof r=="string"?r.match(/^(\d*(\.\d+)?)px$/)?{unit:Sr.Pixel,value:parseFloat(r)}:r.match(/^(\d*(\.\d+)?)%$/)?{unit:Sr.Percent,value:parseFloat(r)}:(console.warn('scrollThreshold format is invalid. Valid formats: "120px", "50%"...'),op):(console.warn("scrollThreshold should be string or number"),op)}var ew=function(r){J1(i,r);function i(s){var l=r.call(this,s)||this;return l.lastScrollTop=0,l.actionTriggered=!1,l.startY=0,l.currentY=0,l.dragging=!1,l.maxPullDownDistance=0,l.getScrollableTarget=function(){return l.props.scrollableTarget instanceof HTMLElement?l.props.scrollableTarget:typeof l.props.scrollableTarget=="string"?document.getElementById(l.props.scrollableTarget):(l.props.scrollableTarget===null&&console.warn(`You are trying to pass scrollableTarget but it is null. This might + happen because the element may not have been added to DOM yet. + See https://github.com/ankeetmaini/react-infinite-scroll-component/issues/59 for more info. + `),null)},l.onStart=function(c){l.lastScrollTop||(l.dragging=!0,c instanceof MouseEvent?l.startY=c.pageY:c instanceof TouchEvent&&(l.startY=c.touches[0].pageY),l.currentY=l.startY,l._infScroll&&(l._infScroll.style.willChange="transform",l._infScroll.style.transition="transform 0.2s cubic-bezier(0,0,0.31,1)"))},l.onMove=function(c){l.dragging&&(c instanceof MouseEvent?l.currentY=c.pageY:c instanceof TouchEvent&&(l.currentY=c.touches[0].pageY),!(l.currentY=Number(l.props.pullDownToRefreshThreshold)&&l.setState({pullToRefreshThresholdBreached:!0}),!(l.currentY-l.startY>l.maxPullDownDistance*1.5)&&l._infScroll&&(l._infScroll.style.overflow="visible",l._infScroll.style.transform="translate3d(0px, "+(l.currentY-l.startY)+"px, 0px)")))},l.onEnd=function(){l.startY=0,l.currentY=0,l.dragging=!1,l.state.pullToRefreshThresholdBreached&&(l.props.refreshFunction&&l.props.refreshFunction(),l.setState({pullToRefreshThresholdBreached:!1})),requestAnimationFrame(function(){l._infScroll&&(l._infScroll.style.overflow="auto",l._infScroll.style.transform="none",l._infScroll.style.willChange="unset")})},l.onScrollListener=function(c){typeof l.props.onScroll=="function"&&setTimeout(function(){return l.props.onScroll&&l.props.onScroll(c)},0);var f=l.props.height||l._scrollableNode?c.target:document.documentElement.scrollTop?document.documentElement:document.body;if(!l.actionTriggered){var p=l.props.inverse?l.isElementAtTop(f,l.props.scrollThreshold):l.isElementAtBottom(f,l.props.scrollThreshold);p&&l.props.hasMore&&(l.actionTriggered=!0,l.setState({showLoader:!0}),l.props.next&&l.props.next()),l.lastScrollTop=f.scrollTop}},l.state={showLoader:!1,pullToRefreshThresholdBreached:!1,prevDataLength:s.dataLength},l.throttledOnScrollListener=Z1(150,l.onScrollListener).bind(l),l.onStart=l.onStart.bind(l),l.onMove=l.onMove.bind(l),l.onEnd=l.onEnd.bind(l),l}return i.prototype.componentDidMount=function(){if(typeof this.props.dataLength>"u")throw new Error('mandatory prop "dataLength" is missing. The prop is needed when loading more content. Check README.md for usage');if(this._scrollableNode=this.getScrollableTarget(),this.el=this.props.height?this._infScroll:this._scrollableNode||window,this.el&&this.el.addEventListener("scroll",this.throttledOnScrollListener),typeof this.props.initialScrollY=="number"&&this.el&&this.el instanceof HTMLElement&&this.el.scrollHeight>this.props.initialScrollY&&this.el.scrollTo(0,this.props.initialScrollY),this.props.pullDownToRefresh&&this.el&&(this.el.addEventListener("touchstart",this.onStart),this.el.addEventListener("touchmove",this.onMove),this.el.addEventListener("touchend",this.onEnd),this.el.addEventListener("mousedown",this.onStart),this.el.addEventListener("mousemove",this.onMove),this.el.addEventListener("mouseup",this.onEnd),this.maxPullDownDistance=this._pullDown&&this._pullDown.firstChild&&this._pullDown.firstChild.getBoundingClientRect().height||0,this.forceUpdate(),typeof this.props.refreshFunction!="function"))throw new Error(`Mandatory prop "refreshFunction" missing. + Pull Down To Refresh functionality will not work + as expected. Check README.md for usage'`)},i.prototype.componentWillUnmount=function(){this.el&&(this.el.removeEventListener("scroll",this.throttledOnScrollListener),this.props.pullDownToRefresh&&(this.el.removeEventListener("touchstart",this.onStart),this.el.removeEventListener("touchmove",this.onMove),this.el.removeEventListener("touchend",this.onEnd),this.el.removeEventListener("mousedown",this.onStart),this.el.removeEventListener("mousemove",this.onMove),this.el.removeEventListener("mouseup",this.onEnd)))},i.prototype.componentDidUpdate=function(s){this.props.dataLength!==s.dataLength&&(this.actionTriggered=!1,this.setState({showLoader:!1}))},i.getDerivedStateFromProps=function(s,l){var c=s.dataLength!==l.prevDataLength;return c?Ao(Ao({},l),{prevDataLength:s.dataLength}):null},i.prototype.isElementAtTop=function(s,l){l===void 0&&(l=.8);var c=s===document.body||s===document.documentElement?window.screen.availHeight:s.clientHeight,f=ip(l);return f.unit===Sr.Pixel?s.scrollTop<=f.value+c-s.scrollHeight+1:s.scrollTop<=f.value/100+c-s.scrollHeight+1},i.prototype.isElementAtBottom=function(s,l){l===void 0&&(l=.8);var c=s===document.body||s===document.documentElement?window.screen.availHeight:s.clientHeight,f=ip(l);return f.unit===Sr.Pixel?s.scrollTop+c>=s.scrollHeight-f.value:s.scrollTop+c>=f.value/100*s.scrollHeight},i.prototype.render=function(){var s=this,l=Ao({height:this.props.height||"auto",overflow:"auto",WebkitOverflowScrolling:"touch"},this.props.style),c=this.props.hasChildren||!!(this.props.children&&this.props.children instanceof Array&&this.props.children.length),f=this.props.pullDownToRefresh&&this.props.height?{overflow:"auto"}:{};return gt.createElement("div",{style:f,className:"infinite-scroll-component__outerdiv"},gt.createElement("div",{className:"infinite-scroll-component "+(this.props.className||""),ref:function(p){return s._infScroll=p},style:l},this.props.pullDownToRefresh&>.createElement("div",{style:{position:"relative"},ref:function(p){return s._pullDown=p}},gt.createElement("div",{style:{position:"absolute",left:0,right:0,top:-1*this.maxPullDownDistance}},this.state.pullToRefreshThresholdBreached?this.props.releaseToRefreshContent:this.props.pullDownToRefreshContent)),this.props.children,!this.state.showLoader&&!c&&this.props.hasMore&&this.props.loader,this.state.showLoader&&this.props.hasMore&&this.props.loader,!this.props.hasMore&&this.props.endMessage))},i}(ie.Component);const tw=r=>r<1024?r+" B":r<1024*1024?(r/1024).toFixed(2)+" KB":r<1024*1024*1024?(r/(1024*1024)).toFixed(2)+" MB":(r/(1024*1024*1024)).toFixed(2)+" GB";function nw({channel:r}){const{messages:i,fetchMessages:s,loadMoreMessages:l,pagination:c,startPolling:f,stopPolling:p}=sh(),{binaryContents:g,fetchBinaryContent:x}=Yn();ie.useEffect(()=>{if(r!=null&&r.id)return s(r.id),f(r.id),()=>{p(r.id)}},[r==null?void 0:r.id,s,f,p]),ie.useEffect(()=>{i.forEach(I=>{var _;(_=I.attachments)==null||_.forEach(C=>{g[C.id]||x(C.id)})})},[i,g,x]);const v=async I=>{try{const{url:_,fileName:C}=I,O=document.createElement("a");O.href=_,O.download=C,O.style.display="none",document.body.appendChild(O);try{const B=await(await window.showSaveFilePicker({suggestedName:I.fileName,types:[{description:"Files",accept:{"*/*":[".txt",".pdf",".doc",".docx",".xls",".xlsx",".jpg",".jpeg",".png",".gif"]}}]})).createWritable(),Q=await(await fetch(_)).blob();await B.write(Q),await B.close()}catch(F){F.name!=="AbortError"&&O.click()}document.body.removeChild(O),window.URL.revokeObjectURL(_)}catch(_){console.error("파일 다운로드 실패:",_)}},S=I=>I!=null&&I.length?I.map(_=>{const C=g[_.id];return C?C.contentType.startsWith("image/")?h.jsx(np,{children:h.jsx(U1,{href:"#",onClick:F=>{F.preventDefault(),v(C)},children:h.jsx("img",{src:C.url,alt:C.fileName})})},C.url):h.jsx(np,{children:h.jsxs(F1,{href:"#",onClick:F=>{F.preventDefault(),v(C)},children:[h.jsx(B1,{children:h.jsxs("svg",{width:"40",height:"40",viewBox:"0 0 40 40",fill:"none",children:[h.jsx("path",{d:"M8 3C8 1.89543 8.89543 1 10 1H22L32 11V37C32 38.1046 31.1046 39 30 39H10C8.89543 39 8 38.1046 8 37V3Z",fill:"#0B93F6",fillOpacity:"0.1"}),h.jsx("path",{d:"M22 1L32 11H24C22.8954 11 22 10.1046 22 9V1Z",fill:"#0B93F6",fillOpacity:"0.3"}),h.jsx("path",{d:"M13 19H27M13 25H27M13 31H27",stroke:"#0B93F6",strokeWidth:"2",strokeLinecap:"round"})]})}),h.jsxs($1,{children:[h.jsx(H1,{children:C.fileName}),h.jsx(b1,{children:tw(C.size)})]})]})},C.url):null}):null,A=I=>new Date(I).toLocaleTimeString(),R=()=>{r!=null&&r.id&&l(r.id)};return h.jsx(R1,{children:h.jsx("div",{id:"scrollableDiv",style:{height:"100%",overflow:"auto",display:"flex",flexDirection:"column-reverse"},children:h.jsx(ew,{dataLength:i.length,next:R,hasMore:c.hasNext,loader:h.jsx("h4",{style:{textAlign:"center"},children:"메시지를 불러오는 중..."}),scrollableTarget:"scrollableDiv",style:{display:"flex",flexDirection:"column-reverse"},inverse:!0,endMessage:h.jsx("p",{style:{textAlign:"center"},children:h.jsx("b",{children:c.currentPage>0?"모든 메시지를 불러왔습니다":""})}),children:h.jsx(P1,{children:[...i].reverse().map(I=>{var C;const _=I.author;return h.jsxs(_1,{children:[h.jsx(T1,{children:h.jsx(rn,{src:_&&_.profile?(C=g[_.profile.id])==null?void 0:C.url:zt,alt:_&&_.username||"알 수 없음"})}),h.jsxs("div",{children:[h.jsxs(I1,{children:[h.jsx(N1,{children:_&&_.username||"알 수 없음"}),h.jsx(O1,{children:A(I.createdAt)})]}),h.jsx(L1,{children:I.content}),S(I.attachments)]})]},I.id)})})})})})}function rw({channel:r}){return r?h.jsxs(m1,{children:[h.jsx(Q1,{channel:r}),h.jsx(nw,{channel:r}),h.jsx(X1,{channel:r})]}):h.jsx(y1,{children:h.jsxs(v1,{children:[h.jsx(w1,{children:"👋"}),h.jsx(x1,{children:"채널을 선택해주세요"}),h.jsxs(S1,{children:["왼쪽의 채널 목록에서 채널을 선택하여",h.jsx("br",{}),"대화를 시작하세요."]})]})})}function ow(r,i="yyyy-MM-dd HH:mm:ss"){if(!r||!(r instanceof Date)||isNaN(r.getTime()))return"";const s=r.getFullYear(),l=String(r.getMonth()+1).padStart(2,"0"),c=String(r.getDate()).padStart(2,"0"),f=String(r.getHours()).padStart(2,"0"),p=String(r.getMinutes()).padStart(2,"0"),g=String(r.getSeconds()).padStart(2,"0");return i.replace("yyyy",s.toString()).replace("MM",l).replace("dd",c).replace("HH",f).replace("mm",p).replace("ss",g)}const iw=T.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +`,sw=T.div` + background: ${({theme:r})=>r.colors.background.primary}; + border-radius: 8px; + width: 500px; + max-width: 90%; + padding: 24px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +`,lw=T.div` + display: flex; + align-items: center; + margin-bottom: 16px; +`,uw=T.div` + color: ${({theme:r})=>r.colors.status.error}; + font-size: 24px; + margin-right: 12px; +`,aw=T.h3` + color: ${({theme:r})=>r.colors.text.primary}; + margin: 0; + font-size: 18px; +`,cw=T.div` + background: ${({theme:r})=>r.colors.background.tertiary}; + color: ${({theme:r})=>r.colors.text.muted}; + padding: 2px 8px; + border-radius: 4px; + font-size: 14px; + margin-left: auto; +`,fw=T.p` + color: ${({theme:r})=>r.colors.text.secondary}; + margin-bottom: 20px; + line-height: 1.5; + font-weight: 500; +`,dw=T.div` + margin-bottom: 20px; + background: ${({theme:r})=>r.colors.background.secondary}; + border-radius: 6px; + padding: 12px; +`,wo=T.div` + display: flex; + margin-bottom: 8px; + font-size: 14px; +`,xo=T.span` + color: ${({theme:r})=>r.colors.text.muted}; + min-width: 100px; +`,So=T.span` + color: ${({theme:r})=>r.colors.text.secondary}; + word-break: break-word; +`,pw=T.button` + background: ${({theme:r})=>r.colors.brand.primary}; + color: white; + border: none; + border-radius: 4px; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + width: 100%; + + &:hover { + background: ${({theme:r})=>r.colors.brand.hover}; + } +`;function hw({isOpen:r,onClose:i,error:s}){var R,I;if(!r)return null;const l=(R=s==null?void 0:s.response)==null?void 0:R.data,c=(l==null?void 0:l.status)||((I=s==null?void 0:s.response)==null?void 0:I.status)||"오류",f=(l==null?void 0:l.code)||"",p=(l==null?void 0:l.message)||(s==null?void 0:s.message)||"알 수 없는 오류가 발생했습니다.",g=l!=null&&l.timestamp?new Date(l.timestamp):new Date,x=ow(g),v=(l==null?void 0:l.exceptionType)||"",S=(l==null?void 0:l.details)||{},A=(l==null?void 0:l.requestId)||"";return h.jsx(iw,{onClick:i,children:h.jsxs(sw,{onClick:_=>_.stopPropagation(),children:[h.jsxs(lw,{children:[h.jsx(uw,{children:"⚠️"}),h.jsx(aw,{children:"오류가 발생했습니다"}),h.jsxs(cw,{children:[c,f?` (${f})`:""]})]}),h.jsx(fw,{children:p}),h.jsxs(dw,{children:[h.jsxs(wo,{children:[h.jsx(xo,{children:"시간:"}),h.jsx(So,{children:x})]}),A&&h.jsxs(wo,{children:[h.jsx(xo,{children:"요청 ID:"}),h.jsx(So,{children:A})]}),f&&h.jsxs(wo,{children:[h.jsx(xo,{children:"에러 코드:"}),h.jsx(So,{children:f})]}),v&&h.jsxs(wo,{children:[h.jsx(xo,{children:"예외 유형:"}),h.jsx(So,{children:v})]}),Object.keys(S).length>0&&h.jsxs(wo,{children:[h.jsx(xo,{children:"상세 정보:"}),h.jsx(So,{children:Object.entries(S).map(([_,C])=>h.jsxs("div",{children:[_,": ",String(C)]},_))})]})]}),h.jsx(pw,{onClick:i,children:"확인"})]})})}const mw=T.div` + width: 240px; + background: ${ee.colors.background.secondary}; + border-left: 1px solid ${ee.colors.border.primary}; +`,gw=T.div` + padding: 16px; + font-size: 14px; + font-weight: bold; + color: ${ee.colors.text.muted}; + text-transform: uppercase; +`,yw=T.div` + padding: 8px 16px; + display: flex; + align-items: center; + color: ${ee.colors.text.muted}; +`,vw=T(Tr)` + margin-right: 12px; +`;T(rn)``;const ww=T.div` + display: flex; + align-items: center; +`;function xw({member:r}){var l,c,f;const{binaryContents:i,fetchBinaryContent:s}=Yn();return ie.useEffect(()=>{var p;(p=r.profile)!=null&&p.id&&!i[r.profile.id]&&s(r.profile.id)},[(l=r.profile)==null?void 0:l.id,i,s]),h.jsxs(yw,{children:[h.jsxs(vw,{children:[h.jsx(rn,{src:(c=r.profile)!=null&&c.id&&((f=i[r.profile.id])==null?void 0:f.url)||zt,alt:r.username}),h.jsx(Io,{$online:r.online})]}),h.jsx(ww,{children:r.username})]})}function Sw(){const r=nn(c=>c.users),i=nn(c=>c.fetchUsers),s=vt(c=>c.currentUserId);ie.useEffect(()=>{i()},[i]);const l=[...r].sort((c,f)=>c.id===s?-1:f.id===s?1:c.online&&!f.online?-1:!c.online&&f.online?1:c.username.localeCompare(f.username));return h.jsxs(mw,{children:[h.jsxs(gw,{children:["멤버 목록 - ",r.length]}),l.map(c=>h.jsx(xw,{member:c},c.id))]})}function Ew(){const r=vt(C=>C.currentUserId),i=vt(C=>C.logout),s=nn(C=>C.users),{fetchUsers:l,updateUserStatus:c}=nn(),[f,p]=ie.useState(null),[g,x]=ie.useState(null),[v,S]=ie.useState(!1),[A,R]=ie.useState(!0),I=r?s.find(C=>C.id===r):null;ie.useEffect(()=>{(async()=>{try{if(r)try{await c(r),await l()}catch(O){console.warn("사용자 상태 업데이트 실패. 로그아웃합니다.",O),i()}}catch(O){console.error("초기화 오류:",O)}finally{R(!1)}})()},[r,c,l,i]),ie.useEffect(()=>{const C=V=>{x(V),S(!0)},O=()=>{i()},F=as.on("api-error",C),B=as.on("auth-error",O);return()=>{F("api-error",C),B("auth-error",O)}},[i]),ie.useEffect(()=>{let C;if(r){c(r),C=setInterval(()=>{c(r)},3e4);const O=setInterval(()=>{l()},6e4);return()=>{clearInterval(C),clearInterval(O)}}},[r,l,c]);const _=()=>{S(!1),x(null)};return A?h.jsx(Cd,{theme:ee,children:h.jsx(kw,{children:h.jsx(jw,{})})}):h.jsxs(Cd,{theme:ee,children:[I?h.jsxs(Cw,{children:[h.jsx(p1,{currentUser:I,activeChannel:f,onChannelSelect:p}),h.jsx(rw,{channel:f}),h.jsx(Sw,{})]}):h.jsx(h0,{isOpen:!0,onClose:()=>{}}),h.jsx(hw,{isOpen:v,onClose:_,error:g})]})}const Cw=T.div` + display: flex; + height: 100vh; + width: 100vw; + position: relative; +`,kw=T.div` + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; + background-color: ${({theme:r})=>r.colors.background.primary}; +`,jw=T.div` + width: 40px; + height: 40px; + border: 4px solid ${({theme:r})=>r.colors.background.tertiary}; + border-top: 4px solid ${({theme:r})=>r.colors.brand.primary}; + border-radius: 50%; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } +`,lh=document.getElementById("root");if(!lh)throw new Error("Root element not found");hg.createRoot(lh).render(h.jsx(ie.StrictMode,{children:h.jsx(Ew,{})})); diff --git a/src/main/resources/static/assets/index-kQJbKSsj.css b/src/main/resources/static/assets/index-kQJbKSsj.css new file mode 100644 index 000000000..096eb4112 --- /dev/null +++ b/src/main/resources/static/assets/index-kQJbKSsj.css @@ -0,0 +1 @@ +:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}} diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..479bed6a3da0a8dbdd08a51d81b30e4d4fabae89 GIT binary patch literal 1588 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyacIC_6YK2V5m}KU}$JzVE6?TYIwoGP-?)y@G60U!Dv>Mu*Du8ycRt4Yw>0&$ytddU zdTHwA$vlU)7;*ZQn^d>r9eiw}SEV3v&DP3PpZVm?c2D=&D? zJg+7dT;x9cg;(mDqrovi2QemjySudY+_R1aaySb-B8!2p69!>MhFNnYfC{QST^vI! zPM@6=9?WDY()wLtM|S>=KoQ44K~Zk4us5=<8xs!eeY>~&=ly4!jD%AXj+wvro>aU~ zrMO$=?`j4U&ZyW$Je*!Zo0>H2RZVqmn^V&mZ(9Dkv!~|IuDF1RBN|EPJE zX3ok)rzF<3&vZKWEj4ag73&t}uJvVk^<~M;*V0n54#8@&v!WGjE_hAaeAZEF z$~V4aF>{^dUc7o%=f8f9m%*2vzjfI@vJ2Z97)VU5x-s2*r@e{H>FEn3A3Dr3G&8U| z)>wFiQO&|Yl6}UkXAQ>%q$jNWac-tTL*)AEyto|onkmnmcJLf?71w_<>4WODmBMxF zwGM7``txcQgT`x>(tH-DrT2Kg=4LzpNv>|+a@TgYDZ`5^$KJVb`K=%k^tRpoxP|4? zwXb!O5~dXYKYt*j(YSx+#_rP{TNcK=40T|)+k3s|?t||EQTgwGgs{E0Y+(QPL&Wx4 zMP23By&sn`zn7oCQQLp%-(Axm|M=5-u;TlFiTn5B^PWnb%fAPV8r2flh?11Vl2ohY zqEsNoU}Ruqple{LYiJr`U}|M-Vr62aZD3$!V6dZTmJ5o8-29Zxv`X9>PU+TH>UWRL)v7?M$%n`C9>lAm0fo0?Z*WfcHaTFhX${Qqu! zG&Nv5t*kOqGt)Cl7z{0q_!){?fojB&%z>&2&rB)F04ce=Mv()kL=s7fZ)R?4No7GQ z1K3si1$pWAo5K9i%<&BYs$wuSHMcY{Gc&O;(${(hEL0izk<1CstV(4taB`Zm$nFhL zDhx>~G{}=7Ei)$-=zaa%ypo*!bp5o%vdrZCykdPs#ORw@rkW)uCz=~4Cz={1nkQNs oC7PHSBpVtgnwc6|q*&+yb?5=zccWrGsMu%lboFyt=akR{0N~++#sB~S literal 0 HcmV?d00001 diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 000000000..94d9533b4 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,26 @@ + + + + + + Discodeit + + + + + +
+ + From f5450f28783fdf1395a49e345033f684ca76801c Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Wed, 4 Mar 2026 11:51:06 +0900 Subject: [PATCH 03/28] feat(dto): update dto for new api specs + misc: modify docker-compose.yml, application-dev.yml --- docker-compose.yml | 4 +- .../mission/discodeit/JavaApplication.java | 53 ------------------- .../mission/discodeit/dto/AuthServiceDTO.java | 4 +- .../discodeit/dto/ChannelServiceDTO.java | 16 +++--- .../discodeit/dto/MessageServiceDTO.java | 13 +++-- .../discodeit/dto/ReadStatusServiceDTO.java | 3 +- .../BinaryContentServiceDTO.java | 5 +- .../discodeit/dto/user/UserServiceDTO.java | 8 ++- .../dto/userstatus/UserStatusServiceDTO.java | 3 +- .../mission/discodeit/util/RandomUUID.java | 38 ------------- src/main/resources/application-dev.yml | 2 +- 11 files changed, 25 insertions(+), 124 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/JavaApplication.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/util/RandomUUID.java diff --git a/docker-compose.yml b/docker-compose.yml index 68b379b6d..6b0aff22a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,8 @@ services: container_name: discodeit-A restart: always environment: - POSTGRES_USER: jonas - POSTGRES_PASSWORD: jonas + POSTGRES_USER: discodeit_user + POSTGRES_PASSWORD: discodeit1234 POSTGRES_DB: discodeit ports: - "5432:5432" diff --git a/src/main/java/com/sprint/mission/discodeit/JavaApplication.java b/src/main/java/com/sprint/mission/discodeit/JavaApplication.java deleted file mode 100644 index 442f7dfe2..000000000 --- a/src/main/java/com/sprint/mission/discodeit/JavaApplication.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sprint.mission.discodeit; - -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.entity.ChannelType; -import com.sprint.mission.discodeit.entity.Message; -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.repository.ChannelRepository; -import com.sprint.mission.discodeit.repository.MessageRepository; -import com.sprint.mission.discodeit.repository.UserRepository; -import com.sprint.mission.discodeit.repository.file.FileChannelRepository; -import com.sprint.mission.discodeit.repository.file.FileMessageRepository; -import com.sprint.mission.discodeit.repository.file.FileUserRepository; -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.MessageService; -import com.sprint.mission.discodeit.service.UserService; -import com.sprint.mission.discodeit.service.basic.BasicChannelService; -import com.sprint.mission.discodeit.service.basic.BasicMessageService; -import com.sprint.mission.discodeit.service.basic.BasicUserService; - -public class JavaApplication { -// static User setupUser(UserService userService) { -// User user = userService.create(); -// return user; -// } -// -// static Channel setupChannel(ChannelService channelService) { -// Channel channel = channelService.create(ChannelType.PUBLIC); -// return channel; -// } -// -// static void messageCreateTest(MessageService messageService, Channel channel, User author) { -// Message message = messageService.create("안녕하세요."); -// System.out.println("메시지 생성: " + message.getId()); -// } -// -// public static void main(String[] args) { -// // 레포지토리 초기화 -// UserRepository userRepository = new FileUserRepository(); -// ChannelRepository channelRepository = new FileChannelRepository(); -// MessageRepository messageRepository = new FileMessageRepository(); -// -// // 서비스 초기화 -// UserService userService = new BasicUserService(userRepository); -// ChannelService channelService = new BasicChannelService(channelRepository); -// MessageService messageService = new BasicMessageService(messageRepository, channelRepository, userRepository); -// -// // 셋업 -// User user = setupUser(userService); -// Channel channel = setupChannel(channelService); -// // 테스트 -// messageCreateTest(messageService, channel, user); -// } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java index f654be3f2..595594c68 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java @@ -1,8 +1,8 @@ package com.sprint.mission.discodeit.dto; -import lombok.NonNull; +import jakarta.annotation.Nonnull; public interface AuthServiceDTO { - record LoginRequest(@NonNull String username, @NonNull String password) { + record LoginRequest(@Nonnull String username, @Nonnull String password) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java index 4605170fe..9f223ecdf 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java @@ -1,12 +1,13 @@ package com.sprint.mission.discodeit.dto; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; import com.sprint.mission.discodeit.entity.ChannelType; import jakarta.annotation.Nonnull; import lombok.Builder; import java.time.LocalDateTime; import java.util.List; -import java.util.Objects; import java.util.UUID; public interface ChannelServiceDTO { @@ -17,7 +18,8 @@ record PublicChannelCreateRequest(@Nonnull String name, @Nonnull String descript record PrivateChannelCreateRequest(@Nonnull List participantIds) { } - record PublicChannelUpdateRequest(String newName, String newDescription) { + record PublicChannelUpdateRequest(@JsonProperty("newName") String name, + @JsonProperty("newDescription") String description) { } record PublicChannelUpdateCommand(UUID id, String name, String description) { @@ -25,13 +27,7 @@ record PublicChannelUpdateCommand(UUID id, String name, String description) { // todo: error log @Builder - record ChannelResponse(UUID id, String name, String description, ChannelType type, - List participantIds, LocalDateTime createdAt, LocalDateTime updatedAt) { - public ChannelResponse { - if (type == ChannelType.PUBLIC) { - Objects.requireNonNull(name); - Objects.requireNonNull(description); - } - } + record ChannelDto(UUID id, String name, String description, ChannelType type, + List participants, LocalDateTime lastMessageAt) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java index 86b63f589..ddc671d80 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java @@ -1,7 +1,9 @@ package com.sprint.mission.discodeit.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import lombok.Builder; @@ -15,14 +17,15 @@ public interface MessageServiceDTO { record MessageCreateRequest(@Nonnull UUID authorId, @Nonnull UUID channelId, @Nonnull String content) { } - record MessageCreateCommand(UUID authorId, UUID channelId, String content, List attachments) { + record MessageCreateCommand(UUID authorId, UUID channelId, String content, + List attachments) { public MessageCreateCommand(MessageCreateRequest request, List attachments) { this(request.authorId(), request.channelId(), request.content(), attachments.stream().map(BinaryContentCreateRequest::from).toList()); } } - record MessageUpdateRequest(@JsonProperty("newContent") @Nullable String content) { + record MessageUpdateRequest(@JsonProperty("newContent") String content) { } record MessageUpdateCommand(UUID id, String content) { @@ -33,8 +36,8 @@ public MessageUpdateCommand(UUID id, MessageUpdateRequest request) { // todo: error log @Builder - record MessageResponse(UUID id, UUID authorId, UUID channelId, String content, - List attachmentIds, LocalDateTime createdAt, - LocalDateTime updatedAt) { + record MessageDto(UUID id, UserDto author, UUID channelId, String content, + List attachments, LocalDateTime createdAt, + LocalDateTime updatedAt) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java index 4290f3355..62c744bdb 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java @@ -21,7 +21,6 @@ record ReadStatusUpdateCommand(UUID id, LocalDateTime datetime) { // todo: error log @Builder - record ReadStatusResponse(UUID id, UUID userId, UUID channelId, - LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime lastReadAt) { + record ReadStatusDto(UUID id, UUID userId, UUID channelId, LocalDateTime lastReadAt) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java index e6a1431a1..c9e70d14d 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java @@ -1,15 +1,12 @@ package com.sprint.mission.discodeit.dto.binarycontent; import lombok.Builder; -import lombok.NonNull; -import java.time.LocalDateTime; import java.util.UUID; public interface BinaryContentServiceDTO { @Builder - record BinaryContentResponse(@NonNull UUID id, @NonNull String fileName, @NonNull byte[] data, - LocalDateTime createdAt) { + record BinaryContentDto(UUID id, String fileName, int size, String contentType) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java index ec4fd093a..3eaffb3c0 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java @@ -1,10 +1,9 @@ package com.sprint.mission.discodeit.dto.user; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; import lombok.Builder; -import lombok.NonNull; -import java.sql.Timestamp; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; @@ -19,7 +18,6 @@ record UserProfileImageDto(String fileName, byte[] data) {} // todo: error log @Builder - record UserResponse(@NonNull UUID id, @NonNull String username, @NonNull String email, - boolean online, UUID profileId, LocalDateTime createdAt, LocalDateTime updatedAt) { + record UserDto(UUID id, String username, String email, BinaryContentDto profile, boolean online) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java index 2be6dc6a2..1a8ec5412 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java @@ -8,7 +8,6 @@ public interface UserStatusServiceDTO { @Builder - record UserStatusResponse(UUID id, UUID userId, boolean online, LocalDateTime createdAt, LocalDateTime updatedAt, - LocalDateTime lastActiveAt) { + record UserStatusDto(UUID id, UUID userId, LocalDateTime lastActiveAt) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/util/RandomUUID.java b/src/main/java/com/sprint/mission/discodeit/util/RandomUUID.java deleted file mode 100644 index f786c2594..000000000 --- a/src/main/java/com/sprint/mission/discodeit/util/RandomUUID.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sprint.mission.discodeit.util; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; -import org.springframework.util.IdGenerator; - -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; - -public interface RandomUUID { - @Component - @ConditionalOnProperty( - prefix = "discodeit.uuid", - name = "type", - havingValue = "prod" - ) - class RandomGenerator implements IdGenerator { - @Override - public UUID generateId() { - return UUID.randomUUID(); - } - } - - @Component - @ConditionalOnProperty( - prefix = "discodeit.uuid", - name = "type", - havingValue = "dev" - ) - class FixGenerator implements IdGenerator { - private final AtomicInteger count = new AtomicInteger(); - - @Override - public UUID generateId() { - return UUID.fromString(String.format("00000000-0000-0000-0000-%012d", count.incrementAndGet())); - } - } -} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 70bceda66..29ebffdd0 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,7 +7,7 @@ discodeit: springdoc: swagger-ui: - url: /api-docs_1.1.json + url: /api/api-docs_1.1.json spring: datasource: From e6eab0ba9bb904a8fe27bcb973037a3a1c15851e Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Wed, 4 Mar 2026 15:29:55 +0900 Subject: [PATCH 04/28] feat(db, application.yml): init schema.sql, update application-dev.yml --- db/schema.sql | 96 ++++++++++++++++++++++++++ src/main/resources/application-dev.yml | 15 ++-- 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 db/schema.sql diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 000000000..e9330a278 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,96 @@ +drop domain if exists NOT_NULL_TZ cascade; +drop table if exists users cascade; +drop table if exists channels cascade; +drop table if exists messages cascade; +drop table if exists binary_contents cascade; +drop table if exists read_statuses cascade; +drop table if exists message_attachments cascade; +drop table if exists user_statuses cascade; + +create domain NOT_NULL_TZ as timestamptz not null; + +create table users +( + id uuid primary key, + created_at NOT_NULL_TZ, + updated_at timestamptz, + username varchar(50) not null, + email varchar(100) not null, + password varchar(60) not null, + profile_id uuid +); + +create table channels +( + id uuid primary key, + created_at NOT_NULL_TZ, + updated_at timestamptz, + name varchar(100), + description varchar(500), + type varchar(10) not null +); + +create table messages +( + id uuid primary key, + created_at NOT_NULL_TZ, + updated_at timestamptz, + content text, + channel_id uuid not null, + author_id uuid not null +); + +create table binary_contents +( + id uuid primary key, + created_at NOT_NULL_TZ, + file_name varchar(255) not null, + size bigint not null, + content_type varchar(100) not null, + bytes bytea not null +); + +create table user_statuses +( + id uuid primary key, + created_at NOT_NULL_TZ, + updated_at timestamptz, + user_id uuid not null, + last_active_at NOT_NULL_TZ +); + +create table read_statuses +( + id uuid primary key, + created_at NOT_NULL_TZ, + updated_at timestamptz, + user_id uuid not null, + channel_id uuid not null, + last_read_at NOT_NULL_TZ +); + +create table message_attachments +( + message_id uuid not null, + attachment_id uuid not null +); + +alter table users + add constraint fk_users_profile_id foreign key (profile_id) references binary_contents (id) on delete set null, + add constraint uk_users_username unique (username), + add constraint uk_users_email unique (email); +alter table channels + add constraint ck_channels_type check ( type in ('PUBLIC', 'PRIVATE') ); +alter table messages + add constraint fk_messages_channel_id foreign key (channel_id) references channels (id) on delete cascade, + add constraint fk_messages_author_id foreign key (author_id) references users (id) on delete set null; +alter table user_statuses + add constraint fk_user_statuses_user_id foreign key (user_id) references users (id) on delete cascade, + add constraint uk_user_statuses_user_id unique (user_id); +alter table read_statuses + add constraint fk_read_statuses_user_id foreign key (user_id) references users (id) on delete cascade, + add constraint fk_read_statuses_channel_id foreign key (channel_id) references channels (id) on delete cascade, + add constraint uk_read_statuses_user_channel_id unique (user_id, channel_id); +alter table message_attachments + add constraint fk_message_attachments_message_id foreign key (message_id) references messages (id) on delete cascade, + add constraint fk_message_attachments_attachment_id foreign key (attachment_id) references binary_contents (id) on delete cascade; \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 29ebffdd0..86051c2c2 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -10,12 +10,19 @@ springdoc: url: /api/api-docs_1.1.json spring: + sql: + init: + mode: always + schema-locations: classpath:db/schema.sql + datasource: url: jdbc:postgresql://localhost:5432/discodeit - username: jonas - password: jonas + username: discodeit_user + password: discodeit1234 + jpa: hibernate: - ddl-auto: create + ddl-auto: none show-sql: true - database: PostgreSQL \ No newline at end of file + database: PostgreSQL + defer-datasource-initialization: false \ No newline at end of file From fa547ccc9b0e9e528e38f17d5fb9772194db5c05 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Thu, 5 Mar 2026 11:13:02 +0900 Subject: [PATCH 05/28] feat(entity): init jpa entity - init jpa entity: UserV2, UserStatusV2, ChannelV2, BinaryContentV2, MessageV2, ReadStatusV2 --- .../discodeit/entity/BinaryContentV2.java | 24 ++++++++++++++ .../mission/discodeit/entity/ChannelV2.java | 20 ++++++++++++ .../mission/discodeit/entity/MessageV2.java | 31 +++++++++++++++++++ .../discodeit/entity/ReadStatusV2.java | 21 +++++++++++++ .../discodeit/entity/UserStatusV2.java | 22 +++++++++++++ .../mission/discodeit/entity/UserV2.java | 28 +++++++++++++++++ .../discodeit/entity/base/BaseEntityV2.java | 23 ++++++++++++++ .../entity/base/BaseUpdatableEntity.java | 14 +++++++++ 8 files changed, 183 insertions(+) create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/UserV2.java create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntityV2.java create mode 100644 src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java diff --git a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java new file mode 100644 index 000000000..88d6d012b --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java @@ -0,0 +1,24 @@ +package com.sprint.mission.discodeit.entity; + +import com.sprint.mission.discodeit.entity.base.BaseEntityV2; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "binary_contents") +public class BinaryContentV2 extends BaseEntityV2 { + @Column(nullable = false) + private String fileName; + + @Column(nullable = false) + private Long size; + + @Column(nullable = false, length = 100) + private String contentType; + + @Column(nullable = false) + private byte[] bytes; +} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java b/src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java new file mode 100644 index 000000000..2635186e1 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.entity; + +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "channels") +public class ChannelV2 extends BaseUpdatableEntity { + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ChannelType type; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String description; +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java b/src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java new file mode 100644 index 000000000..3475c6333 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java @@ -0,0 +1,31 @@ +package com.sprint.mission.discodeit.entity; + +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Entity +@Table(name = "messages") +public class MessageV2 extends BaseUpdatableEntity { + @Column(nullable = false) + private String content; + + @ManyToOne + @JoinColumn(name = "channel_id") + private ChannelV2 channel; + + @ManyToOne + @JoinColumn(name = "author_id") + private UserV2 user; + + @OneToMany + private List attachments = new ArrayList<>(); + + public void addAttachment(BinaryContentV2 attachment) { + this.attachments.add(attachment); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java new file mode 100644 index 000000000..7e9148149 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java @@ -0,0 +1,21 @@ +package com.sprint.mission.discodeit.entity; + +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; + +import java.time.Instant; + +@Entity +@Table(name = "read_statuses") +public class ReadStatusV2 extends BaseUpdatableEntity { + @ManyToOne + @JoinColumn(name = "user_id") + private UserV2 user; + + @ManyToOne + @JoinColumn(name = "channel_id") + private ChannelV2 channel; + + @Column + private Instant lastReadAt; +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java b/src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java new file mode 100644 index 000000000..63c0c15f0 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java @@ -0,0 +1,22 @@ +package com.sprint.mission.discodeit.entity; + +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.Instant; + +@Getter +@Entity +@Table(name = "user_statuses") +public class UserStatusV2 extends BaseUpdatableEntity { + @OneToOne + @JoinColumn(name = "user_id") + private UserV2 user; + + @Column + private Instant lastActiveAt; +} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserV2.java b/src/main/java/com/sprint/mission/discodeit/entity/UserV2.java new file mode 100644 index 000000000..877936fde --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/UserV2.java @@ -0,0 +1,28 @@ +package com.sprint.mission.discodeit.entity; + +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Entity +@Table(name = "users") +public class UserV2 extends BaseUpdatableEntity { + @Column(unique = true, nullable = false, length = 50) + private String username; + + @Column(unique = true, nullable = false, length = 100) + private String email; + + @Column(nullable = false, length = 60) + private String password; + + @OneToOne + @JoinColumn(name = "profile_id") + private BinaryContentV2 profile; + + @Setter + @OneToOne(mappedBy = "users") + private UserStatusV2 status = new UserStatusV2(); +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntityV2.java b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntityV2.java new file mode 100644 index 000000000..b7d648403 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntityV2.java @@ -0,0 +1,23 @@ +package com.sprint.mission.discodeit.entity.base; + +import jakarta.persistence.*; +import lombok.Getter; + +import java.io.Serial; +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Getter +@MappedSuperclass +public abstract class BaseEntityV2 implements Serializable { + @Serial + private static final long serialVersionUID = 2L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private UUID id; + + @Column(updatable = false, nullable = false) + private Instant createdAt = Instant.now(); +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java new file mode 100644 index 000000000..53f1946a4 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java @@ -0,0 +1,14 @@ +package com.sprint.mission.discodeit.entity.base; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +import java.time.Instant; + +@Getter +@MappedSuperclass +public abstract class BaseUpdatableEntity extends BaseEntityV2 { + @Column + private Instant updatedAt; +} From 70cb40954ac57a2c8261369a05dd5526c6bfcb59 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Thu, 5 Mar 2026 11:42:14 +0900 Subject: [PATCH 06/28] feat(entity, repository): convert to jpa entity, jpa repository --- .../mission/discodeit/entity/BaseEntity.java | 14 --- .../discodeit/entity/BinaryContent.java | 45 +++----- .../discodeit/entity/BinaryContentV2.java | 24 ----- .../mission/discodeit/entity/Channel.java | 88 +++------------ .../mission/discodeit/entity/ChannelV2.java | 20 ---- .../mission/discodeit/entity/Message.java | 83 ++++---------- .../mission/discodeit/entity/MessageV2.java | 31 ------ .../mission/discodeit/entity/ReadStatus.java | 56 +++------- .../discodeit/entity/ReadStatusV2.java | 21 ---- .../mission/discodeit/entity/ReadType.java | 5 - .../sprint/mission/discodeit/entity/User.java | 78 ++++---------- .../mission/discodeit/entity/UserStatus.java | 53 ++------- .../discodeit/entity/UserStatusV2.java | 22 ---- .../mission/discodeit/entity/UserV2.java | 28 ----- .../{BaseEntityV2.java => BaseEntity.java} | 2 +- .../entity/base/BaseUpdatableEntity.java | 2 +- .../repository/BinaryContentRepository.java | 5 +- .../repository/ChannelRepository.java | 5 +- .../repository/DomainRepository.java | 23 ---- .../repository/MessageRepository.java | 5 +- .../repository/ReadStatusRepository.java | 5 +- .../discodeit/repository/UserRepository.java | 7 +- .../repository/UserStatusRepository.java | 6 +- .../file/FileBinaryContentRepository.java | 26 ----- .../file/FileChannelRepository.java | 27 ----- .../repository/file/FileDomainRepository.java | 101 ------------------ .../file/FileMessageRepository.java | 28 ----- .../file/FileReadStatusRepository.java | 33 ------ .../repository/file/FileUserRepository.java | 44 -------- .../file/FileUserStatusRepository.java | 45 -------- .../jcf/JCFBinaryContentRepository.java | 27 ----- .../repository/jcf/JCFChannelRepository.java | 27 ----- .../repository/jcf/JCFDomainRepository.java | 52 --------- .../repository/jcf/JCFMessageRepository.java | 28 ----- .../jcf/JCFReadStatusRepository.java | 34 ------ .../repository/jcf/JCFUserRepository.java | 45 -------- .../jcf/JCFUserStatusRepository.java | 46 -------- 37 files changed, 110 insertions(+), 1081 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/BaseEntity.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/ReadType.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/entity/UserV2.java rename src/main/java/com/sprint/mission/discodeit/entity/base/{BaseEntityV2.java => BaseEntity.java} (88%) delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/DomainRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/file/FileBinaryContentRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/file/FileDomainRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFBinaryContentRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFChannelRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFDomainRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFMessageRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFReadStatusRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserRepository.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserStatusRepository.java diff --git a/src/main/java/com/sprint/mission/discodeit/entity/BaseEntity.java b/src/main/java/com/sprint/mission/discodeit/entity/BaseEntity.java deleted file mode 100644 index 5870e51f2..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/BaseEntity.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -import java.util.function.Consumer; - -public abstract class BaseEntity { - - protected boolean updateIfChanged(T current, T next, Consumer action) { - if (current == null || current.equals(next)) { - return false; - } - action.accept(next); - return true; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java index ba6736b27..7af3e504f 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java @@ -1,37 +1,24 @@ package com.sprint.mission.discodeit.entity; -import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; -import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.entity.base.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; import lombok.Getter; -import lombok.RequiredArgsConstructor; -import java.io.Serial; -import java.io.Serializable; -import java.time.Instant; -import java.util.UUID; +@Getter +@Entity +@Table(name = "binary_contents") +public class BinaryContent extends BaseEntity { + @Column(nullable = false) + private String fileName; -@RequiredArgsConstructor -public class BinaryContent implements Serializable { - @Serial - private static final long serialVersionUID = 1L; + @Column(nullable = false) + private Long size; - @Getter - private final UUID id = UUID.randomUUID(); - private final Instant createdAt = Instant.now(); - private final String fileName; - private final byte[] data; + @Column(nullable = false, length = 100) + private String contentType; - public BinaryContent(BinaryContentCreateRequest model) { - this(model.fileName(), model.data()); - } - - public BinaryContentResponse toResponse() { - return BinaryContentResponse.builder() - .id(id) - .fileName(fileName) - .data(data) - .createdAt(TimeConverter.toDateTime(createdAt)) - .build(); - } + @Column(nullable = false) + private byte[] bytes; } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java deleted file mode 100644 index 88d6d012b..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContentV2.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -import com.sprint.mission.discodeit.entity.base.BaseEntityV2; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Getter; - -@Getter -@Entity -@Table(name = "binary_contents") -public class BinaryContentV2 extends BaseEntityV2 { - @Column(nullable = false) - private String fileName; - - @Column(nullable = false) - private Long size; - - @Column(nullable = false, length = 100) - private String contentType; - - @Column(nullable = false) - private byte[] bytes; -} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java index edbb8a4a7..a4a1e71cb 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java @@ -1,84 +1,20 @@ package com.sprint.mission.discodeit.entity; -import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.ChannelResponse; +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; import lombok.Getter; -import java.io.Serial; -import java.io.Serializable; -import java.time.Instant; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -public class Channel extends BaseEntity implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - @Getter - private final UUID id; - private final Instant createdAt = Instant.now(); - private Instant updatedAt = Instant.now(); +@Getter +@Entity +@Table(name = "channels") +public class Channel extends BaseUpdatableEntity { + @Column(nullable = false) + @Enumerated(EnumType.STRING) private ChannelType type; - private String channelName; - private String description; - private Set participantIds; - - private Channel(UUID id) { - this.id = id; - this.participantIds = new HashSet<>(); - } - - public Channel(UUID id, List participantIds) { - this(id); - this.type = ChannelType.PRIVATE; - this.channelName = null; - this.description = null; - this.participantIds = Set.copyOf(participantIds); - } - - public Channel(UUID id, String channelName, String description) { - this(id); - this.type = ChannelType.PUBLIC; - this.channelName = channelName; - this.description = description; - } - public boolean matchChannelType(ChannelType type) { - return this.type == type; - } + @Column(nullable = false) + private String name; - public boolean isPrivateMember(UUID userId) { - if (type == ChannelType.PUBLIC) { - return false; - } - return participantIds.contains(userId); - } - - public ChannelResponse toResponse() { - return ChannelResponse.builder() - .id(id) - .name(channelName) - .description(description) - .type(type) - .participantIds(List.copyOf(participantIds)) - .createdAt(TimeConverter.toDateTime(createdAt)) - .updatedAt(TimeConverter.toDateTime(updatedAt)) - .build(); - } - - public void update(String newName, String newDescription) { - boolean hasUpdated = false; - hasUpdated |= updateIfChanged(this.channelName, newName, val -> this.channelName = val); - hasUpdated |= updateIfChanged(this.description, newDescription, val -> this.description = val); - - if (hasUpdated) { - this.updatedAt = Instant.now(); - } - } - - public boolean isVisibleTo(UUID userId) { - return this.matchChannelType(ChannelType.PUBLIC) || this.isPrivateMember(userId); - } + @Column(nullable = false) + private String description; } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java b/src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java deleted file mode 100644 index 2635186e1..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/ChannelV2.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; -import jakarta.persistence.*; -import lombok.Getter; - -@Getter -@Entity -@Table(name = "channels") -public class ChannelV2 extends BaseUpdatableEntity { - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private ChannelType type; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String description; -} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Message.java b/src/main/java/com/sprint/mission/discodeit/entity/Message.java index d5b7a7394..866e91ab7 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Message.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Message.java @@ -1,78 +1,31 @@ package com.sprint.mission.discodeit.entity; -import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.MessageServiceDTO.MessageResponse; +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; import lombok.Getter; -import java.io.Serial; -import java.io.Serializable; -import java.time.Instant; -import java.util.HashSet; +import java.util.ArrayList; import java.util.List; -import java.util.Set; -import java.util.UUID; -public class Message extends BaseEntity implements Serializable, Comparable { - @Serial - private static final long serialVersionUID = 1L; - - @Getter - private final UUID id; - private final Instant createdAt = Instant.now(); - private Instant updatedAt = Instant.now(); +@Getter +@Entity +@Table(name = "messages") +public class Message extends BaseUpdatableEntity { + @Column(nullable = false) private String content; - private final UUID channelId; - private final UUID authorId; - private final Set attachmentIds = new HashSet<>(); - - public Message(UUID id, String content, UUID channelId, UUID authorId, List attachmentIds) { - this.id = id; - this.content = content; - this.channelId = channelId; - this.authorId = authorId; - this.attachmentIds.addAll(attachmentIds); - } - public boolean isAuthor(UUID userId) { - return this.authorId.equals(userId); - } - - public boolean isInChannel(UUID channelId) { - return this.channelId.equals(channelId); - } + @ManyToOne + @JoinColumn(name = "channel_id") + private Channel channel; - public void update(String newContent, List attachmentIds) { - boolean hasUpdated = false; - hasUpdated |= updateIfChanged(this.content, newContent, val -> this.content = newContent); - hasUpdated |= addAttachments(attachmentIds); + @ManyToOne + @JoinColumn(name = "author_id") + private User user; - if (hasUpdated) { - this.updatedAt = Instant.now(); - } - } - - @Override - public int compareTo(Message m) { - return createdAt.compareTo(m.createdAt); - } - - public MessageResponse toResponse() { - return MessageResponse.builder() - .id(id) - .content(content) - .channelId(channelId) - .authorId(authorId) - .attachmentIds(List.copyOf(attachmentIds)) - .createdAt(TimeConverter.toDateTime(createdAt)) - .updatedAt(TimeConverter.toDateTime(updatedAt)) - .build(); - } + @OneToMany + private List attachments = new ArrayList<>(); - private boolean addAttachments(List attachmentIds) { - if (attachmentIds.isEmpty()) { - return false; - } - this.attachmentIds.addAll(attachmentIds); - return true; + public void addAttachment(BinaryContent attachment) { + this.attachments.add(attachment); } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java b/src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java deleted file mode 100644 index 3475c6333..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/MessageV2.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; -import jakarta.persistence.*; -import lombok.Getter; - -import java.util.ArrayList; -import java.util.List; - -@Getter -@Entity -@Table(name = "messages") -public class MessageV2 extends BaseUpdatableEntity { - @Column(nullable = false) - private String content; - - @ManyToOne - @JoinColumn(name = "channel_id") - private ChannelV2 channel; - - @ManyToOne - @JoinColumn(name = "author_id") - private UserV2 user; - - @OneToMany - private List attachments = new ArrayList<>(); - - public void addAttachment(BinaryContentV2 attachment) { - this.attachments.add(attachment); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java index 5e642e932..df6e98b0f 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java @@ -1,51 +1,21 @@ package com.sprint.mission.discodeit.entity; -import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusResponse; -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; -import java.io.Serial; -import java.io.Serializable; import java.time.Instant; -import java.time.LocalDateTime; -import java.util.UUID; -@RequiredArgsConstructor -public class ReadStatus implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - @Getter - private final UUID id = UUID.randomUUID(); - private final UUID userId; - private final UUID channelId; - private final Instant createdAt = Instant.now(); - private Instant updatedAt = Instant.now(); - @NonNull - private Instant lastReadAt; - - public void update(LocalDateTime datetime) { - lastReadAt = TimeConverter.toInstant(datetime); - updatedAt = Instant.now(); - } +@Entity +@Table(name = "read_statuses") +public class ReadStatus extends BaseUpdatableEntity { + @ManyToOne + @JoinColumn(name = "user_id") + private User user; - public boolean matchChannelId(UUID channelId) { - return this.channelId.equals(channelId); - } + @ManyToOne + @JoinColumn(name = "channel_id") + private Channel channel; - public boolean matchUserId(UUID userId) { - return this.userId.equals(userId); - } - - public ReadStatusResponse toResponse() { - return ReadStatusResponse.builder() - .id(id) - .userId(userId) - .channelId(channelId) - .createdAt(TimeConverter.toDateTime(createdAt)) - .updatedAt(TimeConverter.toDateTime(updatedAt)) - .lastReadAt(TimeConverter.toDateTime(lastReadAt)) - .build(); - } + @Column + private Instant lastReadAt; } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java deleted file mode 100644 index 7e9148149..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatusV2.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; -import jakarta.persistence.*; - -import java.time.Instant; - -@Entity -@Table(name = "read_statuses") -public class ReadStatusV2 extends BaseUpdatableEntity { - @ManyToOne - @JoinColumn(name = "user_id") - private UserV2 user; - - @ManyToOne - @JoinColumn(name = "channel_id") - private ChannelV2 channel; - - @Column - private Instant lastReadAt; -} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadType.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadType.java deleted file mode 100644 index d844a2354..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/ReadType.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -public enum ReadType { - READ, UNREAD; -} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/User.java b/src/main/java/com/sprint/mission/discodeit/entity/User.java index 94f004aa3..50e9d93e3 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/User.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/User.java @@ -1,70 +1,28 @@ package com.sprint.mission.discodeit.entity; -import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUpdateDto; +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; -import java.io.Serial; -import java.io.Serializable; -import java.time.Instant; -import java.util.UUID; - -public class User extends BaseEntity implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - @Getter - private final UUID id; - private final Instant createdAt = Instant.now(); - private Instant updatedAt = Instant.now(); +@Getter +@Entity +@Table(name = "users") +public class User extends BaseUpdatableEntity { + @Column(unique = true, nullable = false, length = 50) private String username; - private String email; - private String password; - private UUID profileId; - // todo: add message, channel id list - // todo: add parameters of update, which is messageId, channelId - - public User(UUID id, String username, String email, - String password, UUID profileId) { - this.id = id; - this.username = username; - this.email = email; - this.password = password; - this.profileId = profileId; - } - public boolean matchPassword(String password) { - return this.password.equals(password); - } - - public boolean matchUsername(String username) { - return this.username.equals(username); - } + @Column(unique = true, nullable = false, length = 100) + private String email; - public boolean matchEmail(String email) { - return this.email.equals(email); - } + @Column(nullable = false, length = 60) + private String password; - public void update(UserUpdateDto dto) { - boolean hasUpdated = false; - hasUpdated |= updateIfChanged(this.username, dto.username(), val -> this.username = val); - hasUpdated |= updateIfChanged(this.email, dto.email(), val -> this.email = val); - hasUpdated |= updateIfChanged(this.password, dto.password(), val -> this.password = val); - hasUpdated |= updateIfChanged(this.profileId, dto.profileId(), val -> this.profileId = val); - if (hasUpdated) { - this.updatedAt = Instant.now(); - } - } + @OneToOne + @JoinColumn(name = "profile_id") + private BinaryContent profile; - public UserResponse toResponse(boolean isActive) { - return UserResponse.builder() - .id(id) - .username(username) - .email(email) - .online(isActive) - .profileId(profileId) - .createdAt(TimeConverter.toDateTime(createdAt)) - .updatedAt(TimeConverter.toDateTime(updatedAt)) - .build(); - } + @Setter + @OneToOne(mappedBy = "users") + private UserStatus status = new UserStatus(); } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java index ca4a63185..0dc616e7c 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java @@ -1,50 +1,19 @@ package com.sprint.mission.discodeit.entity; -import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusResponse; +import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; +import jakarta.persistence.*; import lombok.Getter; -import lombok.RequiredArgsConstructor; -import java.io.Serial; -import java.io.Serializable; -import java.time.Duration; import java.time.Instant; -import java.time.LocalDateTime; -import java.util.UUID; -@RequiredArgsConstructor -public class UserStatus implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - private final int ACTIVE_THRESHOLD = 300; - @Getter - private final UUID id = UUID.randomUUID(); - private final UUID userId; - private final Instant createdAt = Instant.now(); - private Instant updatedAt = Instant.now(); - private Instant lastActiveAt = Instant.now(); +@Getter +@Entity +@Table(name = "user_statuses") +public class UserStatus extends BaseUpdatableEntity { + @OneToOne + @JoinColumn(name = "user_id") + private User user; - public boolean matchUserId(UUID userId) { - return this.userId.equals(userId); - } - - public void update(LocalDateTime datetime) { - lastActiveAt = TimeConverter.toInstant(datetime); - updatedAt = Instant.now(); - } - - public boolean isActive() { - return Duration.between(lastActiveAt, Instant.now()).getSeconds() < ACTIVE_THRESHOLD; - } - - public UserStatusResponse toResponse() { - return UserStatusResponse.builder() - .id(id) - .userId(userId) - .createdAt(TimeConverter.toDateTime(createdAt)) - .updatedAt(TimeConverter.toDateTime(updatedAt)) - .lastActiveAt(TimeConverter.toDateTime(lastActiveAt)) - .online(isActive()) - .build(); - } + @Column + private Instant lastActiveAt; } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java b/src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java deleted file mode 100644 index 63c0c15f0..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/UserStatusV2.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.Instant; - -@Getter -@Entity -@Table(name = "user_statuses") -public class UserStatusV2 extends BaseUpdatableEntity { - @OneToOne - @JoinColumn(name = "user_id") - private UserV2 user; - - @Column - private Instant lastActiveAt; -} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserV2.java b/src/main/java/com/sprint/mission/discodeit/entity/UserV2.java deleted file mode 100644 index 877936fde..000000000 --- a/src/main/java/com/sprint/mission/discodeit/entity/UserV2.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sprint.mission.discodeit.entity; - -import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Entity -@Table(name = "users") -public class UserV2 extends BaseUpdatableEntity { - @Column(unique = true, nullable = false, length = 50) - private String username; - - @Column(unique = true, nullable = false, length = 100) - private String email; - - @Column(nullable = false, length = 60) - private String password; - - @OneToOne - @JoinColumn(name = "profile_id") - private BinaryContentV2 profile; - - @Setter - @OneToOne(mappedBy = "users") - private UserStatusV2 status = new UserStatusV2(); -} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntityV2.java b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java similarity index 88% rename from src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntityV2.java rename to src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java index b7d648403..5ded94a04 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntityV2.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java @@ -10,7 +10,7 @@ @Getter @MappedSuperclass -public abstract class BaseEntityV2 implements Serializable { +public abstract class BaseEntity implements Serializable { @Serial private static final long serialVersionUID = 2L; diff --git a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java index 53f1946a4..a9f922071 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java @@ -8,7 +8,7 @@ @Getter @MappedSuperclass -public abstract class BaseUpdatableEntity extends BaseEntityV2 { +public abstract class BaseUpdatableEntity extends BaseEntity { @Column private Instant updatedAt; } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java index bff2f7bdd..8cbdb2e76 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java @@ -1,6 +1,9 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.BinaryContent; +import org.springframework.data.jpa.repository.JpaRepository; -public interface BinaryContentRepository extends DomainRepository { +import java.util.UUID; + +public interface BinaryContentRepository extends JpaRepository { } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java index 343b6402b..5249155b1 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java @@ -1,6 +1,9 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.Channel; +import org.springframework.data.jpa.repository.JpaRepository; -public interface ChannelRepository extends DomainRepository { +import java.util.UUID; + +public interface ChannelRepository extends JpaRepository { } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/DomainRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/DomainRepository.java deleted file mode 100644 index 40a2b7fff..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/DomainRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.repository; - -import java.util.Optional; -import java.util.UUID; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Stream; - -public interface DomainRepository { - T save(T entity); - - Optional findById(UUID id); - - boolean existsById(UUID id); - - void deleteById(UUID id); - - R streamAll(Function, R> action); - - boolean anyMatch(Predicate predicate); - - Stream filter(Predicate predicate); -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java index ca226cf59..7cc098cdd 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java @@ -1,6 +1,9 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.Message; +import org.springframework.data.jpa.repository.JpaRepository; -public interface MessageRepository extends DomainRepository { +import java.util.UUID; + +public interface MessageRepository extends JpaRepository { } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java index 1dd4e8a47..c9478c1ba 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java @@ -1,9 +1,10 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.ReadStatus; +import org.springframework.data.jpa.repository.JpaRepository; import java.util.UUID; -public interface ReadStatusRepository extends DomainRepository { - boolean existsByUserAndChannelId(UUID userId, UUID channelId); +public interface ReadStatusRepository extends JpaRepository { + boolean existsByUserIdAndChannelId(UUID userId, UUID channelId); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java index df1aece2c..40df87501 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java @@ -1,13 +1,12 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; +import java.util.UUID; -public interface UserRepository extends DomainRepository { +public interface UserRepository extends JpaRepository { boolean existsByUsername(String username); boolean existsByEmail(String email); - - List findAll(); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java index 8b56660ad..d79143aca 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java @@ -1,15 +1,13 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.UserStatus; +import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; import java.util.Optional; import java.util.UUID; -public interface UserStatusRepository extends DomainRepository { +public interface UserStatusRepository extends JpaRepository { Optional findByUserId(UUID userId); boolean existsByUserId(UUID userId); - - List findAll(); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileBinaryContentRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileBinaryContentRepository.java deleted file mode 100644 index 6082abf59..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileBinaryContentRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.entity.BinaryContent; -import com.sprint.mission.discodeit.repository.BinaryContentRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.nio.file.Paths; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "file" -) -public class FileBinaryContentRepository extends FileDomainRepository implements BinaryContentRepository { - public FileBinaryContentRepository() { - super(Paths.get(System.getProperty("user.dir"), "file-data-map", "BinaryContent"), - ".bc"); - } - - @Override - public BinaryContent save(BinaryContent entity) { - return save(entity, BinaryContent::getId); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java deleted file mode 100644 index c4018d853..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.repository.ChannelRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.nio.file.Paths; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "file" -) -public class FileChannelRepository extends FileDomainRepository implements ChannelRepository { - - public FileChannelRepository() { - super(Paths.get(System.getProperty("user.dir"), "file-data-map", "Channel"), - ".chn"); - } - - @Override - public Channel save(Channel channel) { - return save(channel, Channel::getId); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileDomainRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileDomainRepository.java deleted file mode 100644 index 30199dc6a..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileDomainRepository.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.repository.DomainRepository; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Stream; - -public abstract class FileDomainRepository implements DomainRepository { - protected final Path DIRECTORY; - protected final String EXTENSION; - - public FileDomainRepository(Path DIRECTORY, String EXTENSION) { - this.DIRECTORY = DIRECTORY; - this.EXTENSION = EXTENSION; - try { - Files.createDirectories(DIRECTORY); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected T save(T entity, Function idExtractor) { - Path path = resolvePath(idExtractor.apply(entity)); - try ( - FileOutputStream fos = new FileOutputStream(path.toFile()); - ObjectOutputStream oos = new ObjectOutputStream(fos) - ) { - oos.writeObject(entity); - return entity; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public Optional findById(UUID id) { - if (existsById(id)) { - Path path = resolvePath(id); - return findByPath(path); - } - return Optional.empty(); - } - - @Override - public boolean existsById(UUID id) { - return Files.exists(resolvePath(id)); - } - - @Override - public void deleteById(UUID id) { - try { - Files.deleteIfExists(resolvePath(id)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings(value = "unchecked") - protected Optional findByPath(Path path) { - try ( - FileInputStream fis = new FileInputStream(path.toFile()); - ObjectInputStream ois = new ObjectInputStream(fis) - ) { - // todo: deserialize to T ...? - return Optional.of((T) ois.readObject()); - } catch (IOException | ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - @Override - public R streamAll(Function, R> action) { - try (Stream paths = Files.list(DIRECTORY)) { - Stream stream = paths.map(this::findByPath) - .flatMap(Optional::stream); - return action.apply(stream); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean anyMatch(Predicate predicate) { - return streamAll(stream -> stream.anyMatch(predicate)); - } - - @Override - public Stream filter(Predicate predicate) { - return streamAll(stream -> stream.filter(predicate)); - } - - private Path resolvePath(UUID id) { - return DIRECTORY.resolve(id + EXTENSION); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java deleted file mode 100644 index eaca5f0b1..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.entity.Message; -import com.sprint.mission.discodeit.repository.MessageRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.nio.file.Paths; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "file" -) -public class FileMessageRepository extends FileDomainRepository implements MessageRepository { - - public FileMessageRepository() { - super(Paths.get(System.getProperty("user.dir"), "file-data-map", "Message"), - ".msg"); - } - - @Override - public Message save(Message message) { - return save(message, Message::getId); - } - -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java deleted file mode 100644 index 69e28c388..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.entity.ReadStatus; -import com.sprint.mission.discodeit.repository.ReadStatusRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.nio.file.Paths; -import java.util.UUID; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "file" -) -public class FileReadStatusRepository extends FileDomainRepository implements ReadStatusRepository { - public FileReadStatusRepository() { - super(Paths.get(System.getProperty("user.dir"), "file-data-map", "ReadStatus"), - ".rs"); - } - - @Override - public ReadStatus save(ReadStatus entity) { - return save(entity, ReadStatus::getId); - } - - @Override - public boolean existsByUserAndChannelId(UUID userId, UUID channelId) { - return filter(status -> status.matchUserId(userId)) - .anyMatch(status -> status.matchChannelId(channelId)); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java deleted file mode 100644 index 534d94fa3..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.repository.UserRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Stream; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "file" -) -public class FileUserRepository extends FileDomainRepository implements UserRepository { - - public FileUserRepository() { - super(Paths.get(System.getProperty("user.dir"), "file-data-map", "User"), - ".user"); - } - - @Override - public User save(User user) { - return save(user, User::getId); - } - - @Override - public List findAll() { - return streamAll(Stream::toList); - } - - @Override - public boolean existsByUsername(String username) { - return anyMatch(user -> user.matchUsername(username)); - } - - @Override - public boolean existsByEmail(String email) { - return anyMatch(user -> user.matchEmail(email)); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java deleted file mode 100644 index 3345b76ca..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.entity.UserStatus; -import com.sprint.mission.discodeit.repository.UserStatusRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.nio.file.Paths; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Stream; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "file" -) -public class FileUserStatusRepository extends FileDomainRepository implements UserStatusRepository { - public FileUserStatusRepository() { - super(Paths.get(System.getProperty("user.dir"), "file-data-map", "UserStatus"), - ".us"); - } - - @Override - public UserStatus save(UserStatus userStatus) { - return save(userStatus, UserStatus::getId); - } - - @Override - public List findAll() { - return streamAll(Stream::toList); - } - - @Override - public Optional findByUserId(UUID userId) { - return filter(userStatus -> userStatus.matchUserId(userId)).findFirst(); - } - - @Override - public boolean existsByUserId(UUID userId) { - return anyMatch(userStatus -> userStatus.matchUserId(userId)); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFBinaryContentRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFBinaryContentRepository.java deleted file mode 100644 index 3392ac5c1..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFBinaryContentRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf; - -import com.sprint.mission.discodeit.entity.BinaryContent; -import com.sprint.mission.discodeit.repository.BinaryContentRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.util.HashMap; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "jcf" -) -public class JCFBinaryContentRepository extends JCFDomainRepository implements BinaryContentRepository { - - public JCFBinaryContentRepository() { - super(new HashMap<>()); - } - - @Override - public BinaryContent save(BinaryContent entity) { - getData().put(entity.getId(), entity); - return entity; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFChannelRepository.java deleted file mode 100644 index 162c9541d..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFChannelRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf; - -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.repository.ChannelRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.util.HashMap; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "jcf" -) -public class JCFChannelRepository extends JCFDomainRepository implements ChannelRepository { - - public JCFChannelRepository() { - super(new HashMap<>()); - } - - @Override - public Channel save(Channel channel) { - getData().put(channel.getId(), channel); - return channel; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFDomainRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFDomainRepository.java deleted file mode 100644 index d824ec314..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFDomainRepository.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf; - -import com.sprint.mission.discodeit.repository.DomainRepository; - -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Stream; - -public abstract class JCFDomainRepository implements DomainRepository { - private final Map data; - - public JCFDomainRepository(Map data) { - this.data = data; - } - - protected Map getData() { - return data; - } - - @Override - public Optional findById(UUID id) { - return Optional.ofNullable(data.get(id)); - } - - @Override - public boolean existsById(UUID id) { - return data.containsKey(id); - } - - @Override - public void deleteById(UUID id) { - data.remove(id); - } - - @Override - public R streamAll(Function, R> action) { - return action.apply(data.values().stream()); - } - - @Override - public boolean anyMatch(Predicate predicate) { - return streamAll(stream -> stream.anyMatch(predicate)); - } - - @Override - public Stream filter(Predicate predicate) { - return streamAll(stream -> stream.filter(predicate)); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFMessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFMessageRepository.java deleted file mode 100644 index 57f9e60ca..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFMessageRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf; - -import com.sprint.mission.discodeit.entity.Message; -import com.sprint.mission.discodeit.repository.MessageRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.util.HashMap; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "jcf" -) -public class JCFMessageRepository extends JCFDomainRepository implements MessageRepository { - - public JCFMessageRepository() { - super(new HashMap<>()); - } - - @Override - public Message save(Message message) { - getData().put(message.getId(), message); - return message; - } - -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFReadStatusRepository.java deleted file mode 100644 index 483c8a87e..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFReadStatusRepository.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf; - -import com.sprint.mission.discodeit.entity.ReadStatus; -import com.sprint.mission.discodeit.repository.ReadStatusRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.util.HashMap; -import java.util.UUID; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "jcf" -) -public class JCFReadStatusRepository extends JCFDomainRepository implements ReadStatusRepository { - - public JCFReadStatusRepository() { - super(new HashMap<>()); - } - - @Override - public ReadStatus save(ReadStatus entity) { - getData().put(entity.getId(), entity); - return entity; - } - - @Override - public boolean existsByUserAndChannelId(UUID userId, UUID channelId) { - return filter(status -> status.matchUserId(userId)) - .anyMatch(status -> status.matchChannelId(channelId)); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserRepository.java deleted file mode 100644 index 27e6eb29a..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf; - -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.repository.UserRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.util.HashMap; -import java.util.List; -import java.util.stream.Stream; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "jcf" -) -public class JCFUserRepository extends JCFDomainRepository implements UserRepository { - - public JCFUserRepository() { - super(new HashMap<>()); - } - - @Override - public User save(User user) { - getData().put(user.getId(), user); - return user; - } - - @Override - public boolean existsByUsername(String username) { - return anyMatch(user -> user.matchUsername(username)); - } - - @Override - public boolean existsByEmail(String email) { - return anyMatch(user -> user.matchEmail(email)); - } - - @Override - public List findAll() { - return streamAll(Stream::toList); - } - -} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserStatusRepository.java deleted file mode 100644 index 6b9415d3a..000000000 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JCFUserStatusRepository.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf; - -import com.sprint.mission.discodeit.entity.UserStatus; -import com.sprint.mission.discodeit.repository.UserStatusRepository; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Repository; - -import java.util.HashMap; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Stream; - -@Repository -@ConditionalOnProperty( - prefix = "discodeit.repository", - name = "type", - havingValue = "jcf" -) -public class JCFUserStatusRepository extends JCFDomainRepository implements UserStatusRepository { - - public JCFUserStatusRepository() { - super(new HashMap<>()); - } - - @Override - public UserStatus save(UserStatus entity) { - getData().put(entity.getId(), entity); - return entity; - } - - @Override - public Optional findByUserId(UUID userId) { - return filter(status -> status.matchUserId(userId)).findFirst(); - } - - @Override - public boolean existsByUserId(UUID userId) { - return anyMatch(status -> status.matchUserId(userId)); - } - - @Override - public List findAll() { - return streamAll(Stream::toList); - } -} From b2a45f9f5e27fc95d461e7ae782dba5a1a55f107 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Fri, 6 Mar 2026 14:38:18 +0900 Subject: [PATCH 07/28] build(gradle): add mapstruct --- build.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a57aebdd8..b0ab8088e 100644 --- a/build.gradle +++ b/build.gradle @@ -16,15 +16,17 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter' implementation platform('org.springdoc:springdoc-openapi:2.8.5') + compileOnly("org.projectlombok:lombok:1.18.42") + annotationProcessor("org.projectlombok:lombok:1.18.42") + implementation('org.mapstruct:mapstruct:1.6.3') + annotationProcessor('org.mapstruct:mapstruct-processor:1.6.3') + implementation('org.springframework.boot:spring-boot-starter-web') implementation("org.springframework.boot:spring-boot-starter-actuator") implementation('org.springframework.boot:spring-boot-starter-validation') implementation('org.springdoc:springdoc-openapi-starter-webmvc-ui') implementation('org.springframework.boot:spring-boot-starter-data-jpa') - compileOnly("org.projectlombok:lombok:1.18.42") - annotationProcessor("org.projectlombok:lombok:1.18.42") - developmentOnly('org.springframework.boot:spring-boot-devtools') runtimeOnly('org.postgresql:postgresql:42.7.7') From 35ff9b941b7c09f872615e05570a8f1f3ff7525d Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Fri, 6 Mar 2026 15:12:53 +0900 Subject: [PATCH 08/28] feat(dto): renewal UserServiceDto - UserServiceDto: init UserDto, UserUpdateDto, UserCreateDto, UserUniquenessDto, UserResponse --- .../mission/discodeit/dto/UserServiceDTO.java | 22 ---------- .../discodeit/dto/user/UserServiceDTO.java | 32 +++++++++----- .../dto/user/command/UserCreateCommand.java | 38 ----------------- .../dto/user/command/UserUpdateCommand.java | 42 ------------------- 4 files changed, 22 insertions(+), 112 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/UserServiceDTO.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/user/command/UserCreateCommand.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/user/command/UserUpdateCommand.java diff --git a/src/main/java/com/sprint/mission/discodeit/dto/UserServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/UserServiceDTO.java deleted file mode 100644 index d4906ed9d..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/UserServiceDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import com.sprint.mission.discodeit.dto.BinaryContentServiceDTO.BinaryContentCreateRequest; -import lombok.Builder; -import lombok.NonNull; - -import java.util.UUID; - -public interface UserServiceDTO { - record UserCreateRequest(@NonNull String username, @NonNull String email, @NonNull String password, - BinaryContentCreateRequest profileImage) { - } - - record UserUpdateRequest(@NonNull UUID userId, String newUsername, String newEmail, String newPassword, - UUID newProfileId) { - } - - @Builder - record UserResponse(@NonNull UUID userId, @NonNull String username, @NonNull String email, - boolean isActive, UUID profileId) { - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java index 3eaffb3c0..d4f88030d 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java @@ -1,23 +1,35 @@ package com.sprint.mission.discodeit.dto.user; -import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO; import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; -import lombok.Builder; -import java.time.LocalDateTime; import java.util.UUID; public interface UserServiceDTO { - record UserUniquenessDto(String username, String email) { + // todo: error log + record UserResponse(UUID id, String username, String email, boolean online) { } - @Builder - record UserUpdateDto(String username, String email, String password, UUID profileId) {} + record UserDto(UUID id, String username, String email, String password, + BinaryContentDto profile, boolean online) implements UserUpdateDto, UserCreateDto { + } - record UserProfileImageDto(String fileName, byte[] data) {} + interface UserUpdateDto extends UserUniquenessDto { + UUID id(); - // todo: error log - @Builder - record UserDto(UUID id, String username, String email, BinaryContentDto profile, boolean online) { + String password(); + + BinaryContentDto profile(); + } + + interface UserCreateDto extends UserUniquenessDto { + String password(); + + BinaryContentDto profile(); + } + + interface UserUniquenessDto { + String username(); + + String email(); } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/user/command/UserCreateCommand.java b/src/main/java/com/sprint/mission/discodeit/dto/user/command/UserCreateCommand.java deleted file mode 100644 index a995d574a..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/user/command/UserCreateCommand.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sprint.mission.discodeit.dto.user.command; - -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserProfileImageDto; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUniquenessDto; -import com.sprint.mission.discodeit.dto.user.request.UserCreateRequest; -import jakarta.annotation.Nullable; -import lombok.Getter; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; - -@Getter -public class UserCreateCommand { - private final String username; - private final String email; - private final String password; - private final UserProfileImageDto profile; - - public UserCreateCommand(UserCreateRequest request, @Nullable MultipartFile profile) { - this.username = request.username(); - this.email = request.email(); - this.password = request.password(); - - if (profile == null) { - this.profile = null; - } else { - try { - this.profile = new UserProfileImageDto(profile.getOriginalFilename(), profile.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - public UserUniquenessDto getUserUniquenessDto() { - return new UserUniquenessDto(username, email); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/user/command/UserUpdateCommand.java b/src/main/java/com/sprint/mission/discodeit/dto/user/command/UserUpdateCommand.java deleted file mode 100644 index c3ea1d643..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/user/command/UserUpdateCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sprint.mission.discodeit.dto.user.command; - -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserProfileImageDto; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUniquenessDto; -import com.sprint.mission.discodeit.dto.user.request.UserUpdateRequest; -import jakarta.annotation.Nullable; -import lombok.Getter; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.UUID; - -// todo: error log -@Getter -public class UserUpdateCommand { - private final UUID id; - private final String username; - private final String email; - private final String password; - private final UserProfileImageDto profile; - - public UserUpdateCommand(UUID id, UserUpdateRequest request, @Nullable MultipartFile profile) { - this.id = id; - this.username = request.username(); - this.email = request.email(); - this.password = request.password(); - - if (profile == null) { - this.profile = null; - } else { - try { - this.profile = new UserProfileImageDto(profile.getOriginalFilename(), profile.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - public UserUniquenessDto getUserUniquenessDto() { - return new UserUniquenessDto(username, email); - } -} From 54adc9afeda780a6756e724745d32f96232bc291 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Fri, 6 Mar 2026 15:13:59 +0900 Subject: [PATCH 09/28] feat(mapper): init mappers - GlobalMapperConfig: init - BaseMapper: init - UserMapper: init --- .../mission/discodeit/mapper/BaseMapper.java | 11 ++++++ .../mission/discodeit/mapper/UserMapper.java | 34 +++++++++++++++++++ .../mapper/config/GlobalMapperConfig.java | 9 +++++ 3 files changed, 54 insertions(+) create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java new file mode 100644 index 000000000..2a92d9b45 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.mapper; + +import org.mapstruct.MappingTarget; + +public interface BaseMapper { + D toDto(E entity); + + E toEntity(D dto); + + void updateFromDto(@MappingTarget E entity, D dto); +} diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java new file mode 100644 index 000000000..22d045082 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java @@ -0,0 +1,34 @@ +package com.sprint.mission.discodeit.mapper; + +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserCreateDto; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; +import com.sprint.mission.discodeit.dto.user.request.UserCreateRequest; +import com.sprint.mission.discodeit.dto.user.request.UserUpdateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.UUID; + +@Mapper(config = GlobalMapperConfig.class) +public interface UserMapper extends BaseMapper { + BinaryContentMapper profileImageMapper = Mappers.getMapper(BinaryContentMapper.class); + + @Override + @Mapping(source = "isOnline", target = "online") + UserDto toDto(User entity); + + User toEntity(UserCreateDto createDto, BinaryContent profile, UserStatus status); + + UserResponse toResponse(User entity); + + UserDto toDtoFromCreateRequest(UserCreateRequest request, BinaryContentDto profile); + + UserDto toDtoFromUpdateRequest(UUID id, UserUpdateRequest request, BinaryContentDto profile); +} diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java b/src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java new file mode 100644 index 000000000..73a150b73 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.mapper.config; + +import org.mapstruct.MapperConfig; +import org.mapstruct.MappingConstants; +import org.mapstruct.ReportingPolicy; + +@MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface GlobalMapperConfig { +} From aa42fb85f80450bf35fa18d1f95cd01341dd3ec9 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Fri, 6 Mar 2026 15:29:44 +0900 Subject: [PATCH 10/28] feat(service): update UserService - UserService: command -> dto - BasicUserService: remove IdGenerator - BasicUserService: suspending 'find' because not including api specs - BasicUserService: convert entity <-> dto using mapper --- .../discodeit/service/DomainService.java | 4 +- .../discodeit/service/UserService.java | 7 +- .../service/basic/BasicUserService.java | 113 ++++++------------ 3 files changed, 44 insertions(+), 80 deletions(-) diff --git a/src/main/java/com/sprint/mission/discodeit/service/DomainService.java b/src/main/java/com/sprint/mission/discodeit/service/DomainService.java index 8e5a069d5..083d59a1f 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/DomainService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/DomainService.java @@ -3,11 +3,11 @@ import java.util.UUID; public interface DomainService { - F create(C model); + F create(C dto); F find(UUID id); - F update(U model); + F update(U dto); void delete(UUID id); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/UserService.java b/src/main/java/com/sprint/mission/discodeit/service/UserService.java index 1e939d243..8a66657ad 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/UserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/UserService.java @@ -1,15 +1,14 @@ package com.sprint.mission.discodeit.service; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserCreateDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; -import com.sprint.mission.discodeit.dto.user.command.UserCreateCommand; -import com.sprint.mission.discodeit.dto.user.command.UserUpdateCommand; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUpdateDto; import com.sprint.mission.discodeit.dto.user.request.UserFindRequest; import java.util.List; -public interface UserService extends DomainService { +public interface UserService extends DomainService { UserResponse find(UserFindRequest request); List findAll(); - } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java index 0fd41c4fc..e5beca3ab 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java @@ -2,72 +2,65 @@ import com.sprint.mission.discodeit.common.exception.code.ErrorCode; import com.sprint.mission.discodeit.common.exception.custom.APIException; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserProfileImageDto; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserCreateDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUniquenessDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUpdateDto; -import com.sprint.mission.discodeit.dto.user.command.UserCreateCommand; -import com.sprint.mission.discodeit.dto.user.command.UserUpdateCommand; import com.sprint.mission.discodeit.dto.user.request.UserFindRequest; import com.sprint.mission.discodeit.entity.BinaryContent; import com.sprint.mission.discodeit.entity.User; import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.mapper.UserMapper; import com.sprint.mission.discodeit.repository.BinaryContentRepository; import com.sprint.mission.discodeit.repository.UserRepository; import com.sprint.mission.discodeit.repository.UserStatusRepository; import com.sprint.mission.discodeit.service.UserService; -import jakarta.annotation.Nullable; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.util.IdGenerator; import java.util.List; +import java.util.Optional; import java.util.UUID; +//@Transactional, not yet @Service @RequiredArgsConstructor public class BasicUserService extends BasicDomainService implements UserService { private final UserRepository userRepository; private final BinaryContentRepository profileRepository; private final UserStatusRepository userStatusRepository; - private final IdGenerator idGenerator; + private final UserMapper userMapper; + // deprecated ? @Override public UserResponse find(UserFindRequest request) { - User user = userRepository.filter(u -> u.matchUsername(request.username())) - .filter(u -> u.matchPassword(request.password())) - .findFirst() - .orElseThrow(() -> new APIException(ErrorCode.USERNAME_OR_PASSWORD_INCORRECT)); - UserStatus userStatus = findUserStatusByUserId(user.getId()); - return user.toResponse(userStatus.isActive()); + return null; } + // deprecated ? @Override public UserResponse find(UUID userId) { - User user = findById(userId); - UserStatus userStatus = findUserStatusByUserId(user.getId()); - return user.toResponse(userStatus.isActive()); + return null; } @Override public List findAll() { - return userRepository.streamAll( - stream -> stream.map(this::getUserResponse)) + return userRepository.findAll() + .stream() + .map(userMapper::toResponse) .toList(); } @Override - public UserResponse create(UserCreateCommand dto) { - validateUserUniqueness(dto.getUserUniquenessDto()); - - BinaryContent profileImage = registerProfile(dto.getProfile()); - UUID profileImageId = getProfileId(profileImage); - User user = new User(idGenerator.generateId(), dto.getUsername(), dto.getEmail(), - dto.getPassword(), profileImageId); - UserStatus userStatus = new UserStatus(user.getId()); - userStatusRepository.save(userStatus); + public UserResponse create(UserCreateDto dto) { + validateUserUniqueness(dto); + BinaryContent profileImage = registerProfile(dto.profile()); + UserStatus status = new UserStatus(); + User user = userMapper.toEntity(dto, profileImage, status); + userStatusRepository.save(status); userRepository.save(user); - return user.toResponse(userStatus.isActive()); + return userMapper.toResponse(user); } /* @@ -76,40 +69,28 @@ public UserResponse create(UserCreateCommand dto) { * distinguish whether to delete the image or not update it. * */ @Override - public UserResponse update(UserUpdateCommand command) { - User user = findById(command.getId()); - validateUserUniqueness(command.getUserUniquenessDto()); - - UserStatus userStatus = findUserStatusByUserId(command.getId()); - BinaryContent newProfileImage = registerProfile(command.getProfile()); - UUID newProfileId = getProfileId(newProfileImage); - user.update( - UserUpdateDto.builder() - .username(command.getUsername()) - .email(command.getEmail()) - .password(command.getPassword()) - .profileId(newProfileId) - .build()); + public UserResponse update(UserUpdateDto dto) { + validateUserUniqueness(dto); + User user = findById(dto.id()); + BinaryContent newProfileImage = registerProfile(dto.profile()); + user.update(newProfileImage); userRepository.save(user); - return user.toResponse(userStatus.isActive()); + return userMapper.toResponse(user); } + // todo: delete cascade ? @Override - public void delete(UUID userId) { - User user = findById(userId); - UserStatus status = findUserStatusByUserId(userId); - UserResponse userResponse = user.toResponse(status.isActive()); - deleteIfExist(userResponse.profileId(), profileRepository, - () -> new APIException(ErrorCode.BINARYCONTENTID_NOT_FOUND, userResponse.profileId())); - deleteIfExist(status.getId(), userStatusRepository, - () -> new APIException(ErrorCode.USERSTATUSID_NOT_FOUND, status.getId())); - deleteIfExist(userId, userRepository, - () -> new APIException(ErrorCode.USERID_NOT_FOUND, userId)); + public void delete(UUID id) { + User user = findById(id); + profileRepository.delete(user.getProfile()); + userStatusRepository.delete(user.getStatus()); + userRepository.delete(user); } @Override protected User findById(UUID id) { - return findEntityById(id, userRepository, () -> new APIException(ErrorCode.USERID_NOT_FOUND, id)); + return userRepository.findById(id) + .orElseThrow(() -> new APIException(ErrorCode.USERID_NOT_FOUND, id)); } private void validateUserUniqueness(UserUniquenessDto dto) { @@ -121,26 +102,10 @@ private void validateUserUniqueness(UserUniquenessDto dto) { } } - private BinaryContent registerProfile(UserProfileImageDto dto) { - if (dto == null) { - return null; - } - BinaryContent profileImage = new BinaryContent(dto.fileName(), dto.data()); - return profileRepository.save(profileImage); - } - - private UserStatus findUserStatusByUserId(UUID userId) { - return userStatusRepository.findByUserId(userId) - .orElseThrow(() -> new APIException(ErrorCode.USERSTATUS_NOT_FOUND_BY_USERID, userId)); - } - - private UserResponse getUserResponse(User user) { - UserStatus userStatus = findUserStatusByUserId(user.getId()); - return user.toResponse(userStatus.isActive()); - } - - @Nullable - private UUID getProfileId(BinaryContent profileImage) { - return profileImage == null ? null : profileImage.getId(); + private BinaryContent registerProfile(BinaryContentDto profile) { + return Optional.ofNullable(profile) + .map(UserMapper.profileImageMapper::toEntity) + .map(profileRepository::save) + .orElse(null); } } From db7b59700f05b31314637702fc004a6ed9f81a32 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Fri, 6 Mar 2026 16:43:13 +0900 Subject: [PATCH 11/28] feat(controller): update UserController --- .../discodeit/controller/UserController.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/sprint/mission/discodeit/controller/UserController.java b/src/main/java/com/sprint/mission/discodeit/controller/UserController.java index 474730589..5a1a084e0 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/UserController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/UserController.java @@ -1,13 +1,14 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusResponse; -import com.sprint.mission.discodeit.dto.userstatus.command.UserStatusUpdateCommand; -import com.sprint.mission.discodeit.dto.userstatus.request.UserStatusUpdateRequest; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; -import com.sprint.mission.discodeit.dto.user.command.UserCreateCommand; -import com.sprint.mission.discodeit.dto.user.command.UserUpdateCommand; import com.sprint.mission.discodeit.dto.user.request.UserCreateRequest; import com.sprint.mission.discodeit.dto.user.request.UserUpdateRequest; +import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusDto; +import com.sprint.mission.discodeit.dto.userstatus.request.UserStatusUpdateRequest; +import com.sprint.mission.discodeit.mapper.UserMapper; +import com.sprint.mission.discodeit.mapper.UserStatusMapper; import com.sprint.mission.discodeit.service.UserService; import com.sprint.mission.discodeit.service.UserStatusService; import jakarta.annotation.Nullable; @@ -20,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile; import java.util.List; +import java.util.Optional; import java.util.UUID; @RestController @@ -28,6 +30,8 @@ public class UserController { private final UserService userService; private final UserStatusService userStatusService; + private final UserMapper userMapper; + private final UserStatusMapper statusMapper; @GetMapping(value = "/{id}") public ResponseEntity find(@PathVariable UUID id) { @@ -43,8 +47,9 @@ public ResponseEntity> findAll() { public ResponseEntity create( @RequestPart @Valid UserCreateRequest userCreateRequest, @RequestPart @Nullable MultipartFile profile) { - UserCreateCommand command = new UserCreateCommand(userCreateRequest, profile); - return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(command)); + BinaryContentDto profileImageDto = getBinaryContentCreateDto(profile); + UserDto userDto = userMapper.toDtoFromCreateRequest(userCreateRequest, profileImageDto); + return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(userDto)); } @PatchMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -52,8 +57,9 @@ public ResponseEntity update( @PathVariable UUID id, @RequestPart @Valid UserUpdateRequest userUpdateRequest, @RequestPart @Nullable MultipartFile profile) { - UserUpdateCommand command = new UserUpdateCommand(id, userUpdateRequest, profile); - return ResponseEntity.status(HttpStatus.OK).body(userService.update(command)); + BinaryContentDto profileImageDto = getBinaryContentCreateDto(profile); + UserDto userDto = userMapper.toDtoFromUpdateRequest(id, userUpdateRequest, profileImageDto); + return ResponseEntity.status(HttpStatus.OK).body(userService.update(userDto)); } @DeleteMapping(value = "/{id}") @@ -63,9 +69,16 @@ public ResponseEntity delete(@PathVariable UUID id) { } @PatchMapping(value = "/{id}/userStatus") - public ResponseEntity updateActiveStatus(@PathVariable UUID id, - @RequestBody @Valid UserStatusUpdateRequest request) { - UserStatusUpdateCommand command = new UserStatusUpdateCommand(id, request.datetime()); - return ResponseEntity.status(HttpStatus.OK).body(userStatusService.update(command)); + public ResponseEntity updateUserStatus( + @PathVariable UUID id, + @RequestBody UserStatusUpdateRequest request) { + UserStatusDto statusDto = statusMapper.toEntity(id, request); + return ResponseEntity.status(HttpStatus.OK).body(userStatusService.update(statusDto)); + } + + private BinaryContentDto getBinaryContentCreateDto(MultipartFile profile) { + return Optional.ofNullable(profile) + .map(UserMapper.profileImageMapper::toDtoFromFile) + .orElse(null); } } From 02bf6177bcb20459621d62d0ea89e16475d33c3e Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Sat, 7 Mar 2026 23:41:24 +0900 Subject: [PATCH 12/28] feat(User): update entity, mapper, service - BasicDomainService: update - BaseMapper: update - BaseEntity: update - BaseUpdatableEntity: update --- .../sprint/mission/discodeit/entity/User.java | 36 +++++++++++++++++-- .../discodeit/entity/base/BaseEntity.java | 8 +++-- .../entity/base/BaseUpdatableEntity.java | 17 ++++++++- .../mission/discodeit/mapper/BaseMapper.java | 6 ++-- .../mission/discodeit/mapper/UserMapper.java | 6 ++-- .../service/basic/BasicDomainService.java | 25 ++++++------- .../service/basic/BasicUserService.java | 33 ++++++----------- 7 files changed, 84 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/sprint/mission/discodeit/entity/User.java b/src/main/java/com/sprint/mission/discodeit/entity/User.java index 50e9d93e3..ada4b06a8 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/User.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/User.java @@ -1,14 +1,19 @@ package com.sprint.mission.discodeit.entity; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUpdateDto; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Optional; + @Getter +@NoArgsConstructor @Entity @Table(name = "users") -public class User extends BaseUpdatableEntity { +public class User extends BaseUpdatableEntity { @Column(unique = true, nullable = false, length = 50) private String username; @@ -24,5 +29,32 @@ public class User extends BaseUpdatableEntity { @Setter @OneToOne(mappedBy = "users") - private UserStatus status = new UserStatus(); + private UserStatus status; + + public User(String username, String email, String password, + BinaryContent profile, UserStatus status) { + this.username = username; + this.email = email; + this.password = password; + this.profile = profile; + this.status = status; + this.status.setUser(this); + } + + public boolean isOnline() { + return status.isOnline(); + } + + @Override + public void update(UserUpdateDto dto) { + updateIfChanged(username, dto.username(), val -> username = val); + updateIfChanged(email, dto.email(), val -> email = val); + updateIfChanged(password, dto.password(), val -> password = val); + updateIfChanged( + profile, + Optional.ofNullable(dto.profile()) + .map(BinaryContent::new) + .orElse(null), + val -> profile = val); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java index 5ded94a04..b49d9a0f6 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseEntity.java @@ -2,6 +2,8 @@ import jakarta.persistence.*; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.io.Serial; import java.io.Serializable; @@ -10,14 +12,16 @@ @Getter @MappedSuperclass +@EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity implements Serializable { @Serial private static final long serialVersionUID = 2L; @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @GeneratedValue(strategy = GenerationType.UUID) private UUID id; + @CreatedDate @Column(updatable = false, nullable = false) - private Instant createdAt = Instant.now(); + private Instant createdAt; } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java index a9f922071..8b6938c9f 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java @@ -1,14 +1,29 @@ package com.sprint.mission.discodeit.entity.base; import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import lombok.Getter; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.Instant; +import java.util.function.Consumer; @Getter @MappedSuperclass -public abstract class BaseUpdatableEntity extends BaseEntity { +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseUpdatableEntity extends BaseEntity { @Column + @LastModifiedDate private Instant updatedAt; + + protected void updateIfChanged(T before, T after, Consumer action) { + if (after == null || before.equals(after)) { + return; + } + action.accept(after); + } + + public abstract void update(U updateDto); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java index 2a92d9b45..398872f3e 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/BaseMapper.java @@ -1,11 +1,9 @@ package com.sprint.mission.discodeit.mapper; -import org.mapstruct.MappingTarget; - -public interface BaseMapper { +public interface BaseMapper { D toDto(E entity); E toEntity(D dto); - void updateFromDto(@MappingTarget E entity, D dto); + R toResponse(E entity); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java index 22d045082..dd7cd76c8 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java @@ -17,17 +17,15 @@ import java.util.UUID; @Mapper(config = GlobalMapperConfig.class) -public interface UserMapper extends BaseMapper { +public interface UserMapper extends BaseMapper { BinaryContentMapper profileImageMapper = Mappers.getMapper(BinaryContentMapper.class); @Override - @Mapping(source = "isOnline", target = "online") + @Mapping(source = "online", target = "online") UserDto toDto(User entity); User toEntity(UserCreateDto createDto, BinaryContent profile, UserStatus status); - UserResponse toResponse(User entity); - UserDto toDtoFromCreateRequest(UserCreateRequest request, BinaryContentDto profile); UserDto toDtoFromUpdateRequest(UUID id, UserUpdateRequest request, BinaryContentDto profile); diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java index 5c399b18e..f4a1febaf 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java @@ -1,31 +1,32 @@ package com.sprint.mission.discodeit.service.basic; import com.sprint.mission.discodeit.common.exception.custom.APIException; -import com.sprint.mission.discodeit.repository.DomainRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import java.util.UUID; +import java.util.function.Function; import java.util.function.Supplier; public abstract class BasicDomainService { - protected R findEntityById(UUID id, DomainRepository repository, Supplier exception) { - return repository.findById(id).orElseThrow(exception); + protected T getOrThrow(R value, Function> action, Supplier exception) { + return action.apply(value).orElseThrow(exception); } - protected void deleteIfExist(UUID id, DomainRepository repository, Supplier exception) { - if (id == null) { + protected void deleteByIdOrThrow(UUID id, JpaRepository repository, APIException exception) { + if (repository.existsById(id)) { + repository.deleteById(id); return; } - if (!repository.existsById(id)) { - throw exception.get(); - } - repository.deleteById(id); + throw exception; } - protected void ensure(Supplier condition, Supplier exception) { - if (!condition.get()) { - throw exception.get(); + protected void ensure(R value, Function condition, Function exception) { + if (condition.apply(value)) { + return; } + throw exception.apply(value); } protected abstract T findById(UUID id); diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java index e5beca3ab..0fd4e1f98 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java @@ -12,9 +12,7 @@ import com.sprint.mission.discodeit.entity.User; import com.sprint.mission.discodeit.entity.UserStatus; import com.sprint.mission.discodeit.mapper.UserMapper; -import com.sprint.mission.discodeit.repository.BinaryContentRepository; import com.sprint.mission.discodeit.repository.UserRepository; -import com.sprint.mission.discodeit.repository.UserStatusRepository; import com.sprint.mission.discodeit.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -28,8 +26,6 @@ @RequiredArgsConstructor public class BasicUserService extends BasicDomainService implements UserService { private final UserRepository userRepository; - private final BinaryContentRepository profileRepository; - private final UserStatusRepository userStatusRepository; private final UserMapper userMapper; // deprecated ? @@ -55,10 +51,9 @@ public List findAll() { @Override public UserResponse create(UserCreateDto dto) { validateUserUniqueness(dto); - BinaryContent profileImage = registerProfile(dto.profile()); UserStatus status = new UserStatus(); - User user = userMapper.toEntity(dto, profileImage, status); - userStatusRepository.save(status); + BinaryContent profile = registerProfile(dto.profile()); + User user = userMapper.toEntity(dto, profile, status); userRepository.save(user); return userMapper.toResponse(user); } @@ -72,8 +67,7 @@ public UserResponse create(UserCreateDto dto) { public UserResponse update(UserUpdateDto dto) { validateUserUniqueness(dto); User user = findById(dto.id()); - BinaryContent newProfileImage = registerProfile(dto.profile()); - user.update(newProfileImage); + user.update(dto); userRepository.save(user); return userMapper.toResponse(user); } @@ -81,31 +75,26 @@ public UserResponse update(UserUpdateDto dto) { // todo: delete cascade ? @Override public void delete(UUID id) { - User user = findById(id); - profileRepository.delete(user.getProfile()); - userStatusRepository.delete(user.getStatus()); - userRepository.delete(user); + deleteByIdOrThrow(id, userRepository, new APIException(ErrorCode.USERID_NOT_FOUND, id)); } @Override protected User findById(UUID id) { - return userRepository.findById(id) - .orElseThrow(() -> new APIException(ErrorCode.USERID_NOT_FOUND, id)); + return getOrThrow(id, userRepository::findById, () -> new APIException(ErrorCode.USERID_NOT_FOUND, id)); } private void validateUserUniqueness(UserUniquenessDto dto) { - if (userRepository.existsByUsername(dto.username())) { - throw new APIException(ErrorCode.USERNAME_ALREADY_EXIST, dto.username()); - } - if (userRepository.existsByEmail(dto.email())) { - throw new APIException(ErrorCode.EMAIL_ALREADY_EXIST, dto.email()); - } + ensure(dto.username(), + userRepository::existsByUsername, + value -> new APIException(ErrorCode.USERNAME_ALREADY_EXIST, value)); + ensure(dto.email(), + userRepository::existsByEmail, + value -> new APIException(ErrorCode.EMAIL_ALREADY_EXIST, value)); } private BinaryContent registerProfile(BinaryContentDto profile) { return Optional.ofNullable(profile) .map(UserMapper.profileImageMapper::toEntity) - .map(profileRepository::save) .orElse(null); } } From 93c5cdf7b6395da41cc80347fcb60650c0609812 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 9 Mar 2026 11:31:39 +0900 Subject: [PATCH 13/28] feat(Channel): update dto, entity, mapper, service, controller - dto: ChannelServiceDto split by self and several requestDto - Channel: update with jpa - ChannelMapper: init - ChannelService: refactoring - BasicChannelService: update corresponding to jpa repository - ChannelController: update using mapper --- .../controller/ChannelController.java | 70 +++++++++------ .../discodeit/dto/ChannelServiceDTO.java | 33 ------- .../dto/channel/ChannelServiceDTO.java | 45 ++++++++++ .../request/PrivateChannelCreateRequest.java | 13 +++ .../request/PublicChannelCreateRequest.java | 10 +++ .../request/PublicChannelUpdateRequest.java | 12 +++ .../mission/discodeit/entity/Channel.java | 40 ++++++++- .../discodeit/mapper/ChannelMapper.java | 24 +++++ .../discodeit/service/ChannelService.java | 15 ++-- .../service/basic/BasicChannelService.java | 88 +++++++++---------- 10 files changed, 234 insertions(+), 116 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/channel/request/PrivateChannelCreateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelCreateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelUpdateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java diff --git a/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java b/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java index 687337a55..6f99a65ff 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java @@ -1,13 +1,13 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.*; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusCreateRequest; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusResponse; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusUpdateRequest; -import com.sprint.mission.discodeit.entity.ReadType; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelResponse; +import com.sprint.mission.discodeit.dto.channel.request.PrivateChannelCreateRequest; +import com.sprint.mission.discodeit.dto.channel.request.PublicChannelCreateRequest; +import com.sprint.mission.discodeit.dto.channel.request.PublicChannelUpdateRequest; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.MessageService; -import com.sprint.mission.discodeit.service.ReadStatusService; +import com.sprint.mission.discodeit.service.UserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -22,40 +22,56 @@ @RequiredArgsConstructor public class ChannelController { private final ChannelService channelService; - private final MessageService messageService; - private final ReadStatusService readStatusService; + private final UserService userService; @PostMapping(value = "/public") - public ResponseEntity createPublic(@RequestBody @Valid PublicChannelCreateRequest request) { - return ResponseEntity.status(HttpStatus.CREATED).body(channelService.createPublic(request)); + public ResponseEntity create(@RequestBody @Valid PublicChannelCreateRequest request) { + ChannelDto dto = ChannelDto.builder() + .name(request.name()) + .description(request.description()) + .type(request.type()) + .build(); + return ResponseEntity.status(HttpStatus.CREATED) + .body(channelService.createPublic(dto)); } @PostMapping(value = "/private") - public ResponseEntity createPrivate(@RequestBody @Valid PrivateChannelCreateRequest request) { - return ResponseEntity.status(HttpStatus.CREATED).body(channelService.createPrivate(request)); - } - - // deprecated ? - @RequestMapping(value = "/{id}", method = RequestMethod.GET) - public ResponseEntity find(@PathVariable UUID id) { - return ResponseEntity.ok(channelService.find(id)); + public ResponseEntity create(@RequestBody @Valid PrivateChannelCreateRequest request) { + List participants = request.participantIds() + .stream() + .map(userService::find) + .toList(); + ChannelDto dto = ChannelDto.builder() + .participants(participants) + .type(request.type()) + .build(); + return ResponseEntity.status(HttpStatus.CREATED) + .body(channelService.createPrivate(dto)); } @GetMapping public ResponseEntity> findByUserId(@RequestParam UUID userId) { - return ResponseEntity.status(HttpStatus.OK).body(channelService.findAllByUserId(userId)); + return ResponseEntity.status(HttpStatus.OK) + .body(channelService.findAllByUserId(userId)); } @PatchMapping(value = "/{id}") - public ResponseEntity update(@PathVariable UUID id, - @RequestBody PublicChannelUpdateRequest request) { - PublicChannelUpdateCommand command = new PublicChannelUpdateCommand(id, request.newName(), request.newDescription()); - return ResponseEntity.status(HttpStatus.OK).body(channelService.update(command)); + public ResponseEntity update( + @PathVariable UUID id, + @RequestBody PublicChannelUpdateRequest request) { + ChannelDto dto = ChannelDto.builder() + .id(id) + .name(request.name()) + .description(request.description()) + .type(request.type()) + .build(); + return ResponseEntity.status(HttpStatus.OK) + .body(channelService.update(dto)); } - @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) - public ResponseEntity delete(@PathVariable UUID id) { + @DeleteMapping(value = "/{id}") + public ResponseEntity delete(@PathVariable UUID id) { channelService.delete(id); - return ResponseEntity.status(204).body("Channel is removed"); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java deleted file mode 100644 index 9f223ecdf..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/ChannelServiceDTO.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; -import com.sprint.mission.discodeit.entity.ChannelType; -import jakarta.annotation.Nonnull; -import lombok.Builder; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; - -public interface ChannelServiceDTO { - - record PublicChannelCreateRequest(@Nonnull String name, @Nonnull String description) { - } - - record PrivateChannelCreateRequest(@Nonnull List participantIds) { - } - - record PublicChannelUpdateRequest(@JsonProperty("newName") String name, - @JsonProperty("newDescription") String description) { - } - - record PublicChannelUpdateCommand(UUID id, String name, String description) { - } - - // todo: error log - @Builder - record ChannelDto(UUID id, String name, String description, ChannelType type, - List participants, LocalDateTime lastMessageAt) { - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java new file mode 100644 index 000000000..6f8648d2d --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java @@ -0,0 +1,45 @@ +package com.sprint.mission.discodeit.dto.channel; + +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; +import com.sprint.mission.discodeit.entity.ChannelType; +import lombok.Builder; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public interface ChannelServiceDTO { + // todo: error log + @Builder + record ChannelResponse(UUID id, ChannelType type, String name, String description, + List participants, Instant lastMessageAt) { + } + + @Builder + record ChannelDto(UUID id, String name, String description, ChannelType type, + List participants, Instant lastMessageAt) + implements PublicChannelCreateDto, PrivateChannelCreateDto, PublicChannelUpdateDto { + } + + interface PublicChannelCreateDto { + String name(); + + String description(); + + ChannelType type(); + } + + interface PrivateChannelCreateDto { + List participants(); + + ChannelType type(); + } + + interface PublicChannelUpdateDto { + UUID id(); + + String name(); + + String description(); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PrivateChannelCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PrivateChannelCreateRequest.java new file mode 100644 index 000000000..6a00d2a94 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PrivateChannelCreateRequest.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.dto.channel.request; + +import com.sprint.mission.discodeit.entity.ChannelType; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.UUID; + +public record PrivateChannelCreateRequest(@NotNull List participantIds, ChannelType type) { + public PrivateChannelCreateRequest(@NotNull List participantIds) { + this(participantIds, ChannelType.PRIVATE); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelCreateRequest.java new file mode 100644 index 000000000..c85f7defb --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelCreateRequest.java @@ -0,0 +1,10 @@ +package com.sprint.mission.discodeit.dto.channel.request; + +import com.sprint.mission.discodeit.entity.ChannelType; +import jakarta.validation.constraints.NotNull; + +public record PublicChannelCreateRequest(@NotNull String name, @NotNull String description, ChannelType type) { + public PublicChannelCreateRequest(@NotNull String name, @NotNull String description) { + this(name, description, ChannelType.PUBLIC); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelUpdateRequest.java new file mode 100644 index 000000000..a29960f3f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/channel/request/PublicChannelUpdateRequest.java @@ -0,0 +1,12 @@ +package com.sprint.mission.discodeit.dto.channel.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.sprint.mission.discodeit.entity.ChannelType; + +public record PublicChannelUpdateRequest(@JsonProperty("newName") String name, + @JsonProperty("newDescription") String description, + ChannelType type) { + public PublicChannelUpdateRequest(@JsonProperty("newName") String name, @JsonProperty("newDescription") String description) { + this(name, description, ChannelType.PUBLIC); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java index a4a1e71cb..fdab08f06 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java @@ -1,13 +1,23 @@ package com.sprint.mission.discodeit.entity; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PrivateChannelCreateDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PublicChannelCreateDto; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; @Getter +@NoArgsConstructor @Entity @Table(name = "channels") -public class Channel extends BaseUpdatableEntity { +public class Channel extends BaseUpdatableEntity { @Column(nullable = false) @Enumerated(EnumType.STRING) private ChannelType type; @@ -17,4 +27,32 @@ public class Channel extends BaseUpdatableEntity { @Column(nullable = false) private String description; + + private List participants = new ArrayList<>(); + + public Channel(PublicChannelCreateDto dto) { + this.name = dto.name(); + this.description = dto.description(); + this.type = dto.type(); + } + + public Channel(PrivateChannelCreateDto dto) { + participants = dto.participants(); + this.type = dto.type(); + } + + @Override + public void update(ChannelDto dto) { + updateIfChanged(name, dto.name(), val -> name = val); + updateIfChanged(description, dto.description(), val -> description = val); + } + + public boolean matchChannelType(ChannelType type) { + return this.type == type; + } + + public boolean isVisibleTo(UUID userId) { + return type == ChannelType.PUBLIC || + participants.stream().anyMatch(u -> u.id().equals(userId)); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java new file mode 100644 index 000000000..88406172f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java @@ -0,0 +1,24 @@ +package com.sprint.mission.discodeit.mapper; + +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelResponse; +import com.sprint.mission.discodeit.dto.channel.request.PrivateChannelCreateRequest; +import com.sprint.mission.discodeit.dto.channel.request.PublicChannelCreateRequest; +import com.sprint.mission.discodeit.dto.channel.request.PublicChannelUpdateRequest; +import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.UUID; + +@Mapper(config = GlobalMapperConfig.class) +public interface ChannelMapper extends BaseMapper { + UserMapper userMapper = Mappers.getMapper(UserMapper.class); + + ChannelDto toDtoFromRequest(PublicChannelCreateRequest request); + + ChannelDto toDtoFromRequest(PrivateChannelCreateRequest request); + + ChannelDto toDtoFromRequest(UUID id, PublicChannelUpdateRequest request); +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java index 6dfc5d487..b26099d1b 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java @@ -1,10 +1,9 @@ package com.sprint.mission.discodeit.service; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.ChannelResponse; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.PrivateChannelCreateRequest; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.PublicChannelCreateRequest; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.PublicChannelUpdateCommand; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelResponse; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PrivateChannelCreateDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PublicChannelCreateDto; import java.util.List; import java.util.UUID; @@ -14,11 +13,11 @@ public interface ChannelService { List findAllByUserId(UUID userId); - ChannelResponse createPublic(PublicChannelCreateRequest request); + ChannelResponse createPublic(PublicChannelCreateDto dto); - ChannelResponse createPrivate(PrivateChannelCreateRequest request); + ChannelResponse createPrivate(PrivateChannelCreateDto dto); - ChannelResponse update(PublicChannelUpdateCommand command); + ChannelResponse update(ChannelDto dto); void delete(UUID id); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java index 4352e8b9b..850e0cdd8 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java @@ -2,25 +2,24 @@ import com.sprint.mission.discodeit.common.exception.code.ErrorCode; import com.sprint.mission.discodeit.common.exception.custom.APIException; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.ChannelResponse; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.PrivateChannelCreateRequest; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.PublicChannelCreateRequest; -import com.sprint.mission.discodeit.dto.ChannelServiceDTO.PublicChannelUpdateCommand; -import com.sprint.mission.discodeit.dto.MessageServiceDTO.MessageResponse; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelResponse; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PrivateChannelCreateDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PublicChannelCreateDto; import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.entity.ChannelType; import com.sprint.mission.discodeit.entity.Message; import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.mapper.ChannelMapper; import com.sprint.mission.discodeit.repository.ChannelRepository; import com.sprint.mission.discodeit.repository.MessageRepository; import com.sprint.mission.discodeit.repository.ReadStatusRepository; import com.sprint.mission.discodeit.service.ChannelService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.util.IdGenerator; +import java.time.Instant; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @Service @@ -29,79 +28,74 @@ public class BasicChannelService extends BasicDomainService implements private final ChannelRepository channelRepository; private final ReadStatusRepository readStatusRepository; private final MessageRepository messageRepository; - private final IdGenerator idGenerator; + private final ChannelMapper channelMapper; @Override - public ChannelResponse createPublic(PublicChannelCreateRequest request) { - Channel channel = new Channel(idGenerator.generateId(), request.name(), request.description()); + public ChannelResponse createPublic(PublicChannelCreateDto dto) { + Channel channel = new Channel(dto); channelRepository.save(channel); - return channel.toResponse(); + return channelMapper.toResponse(channel); } @Override - public ChannelResponse createPrivate(PrivateChannelCreateRequest request) { - Channel channel = new Channel(idGenerator.generateId(), request.participantIds()); + public ChannelResponse createPrivate(PrivateChannelCreateDto dto) { + Channel channel = new Channel(dto); channelRepository.save(channel); - request.participantIds() + dto.participants() .stream() - .map(userId -> new ReadStatus(userId, channel.getId())) + .map(ChannelMapper.userMapper::toEntity) + .map(user -> new ReadStatus(user, channel, Instant.MIN)) .forEach(readStatusRepository::save); - return channel.toResponse(); + return channelMapper.toResponse(channel); } @Override - public ChannelResponse find(UUID channelId) { - return findById(channelId).toResponse(); + public ChannelResponse find(UUID id) { + return channelMapper.toResponse(findById(id)); } @Override public List findAllByUserId(UUID userId) { - return channelRepository.filter(channel -> channel.isVisibleTo(userId)) - .map(Channel::toResponse) + return channelRepository.findAll() + .stream() + .filter(channel -> channel.isVisibleTo(userId)) + .map(channelMapper::toResponse) .toList(); } @Override - public ChannelResponse update(PublicChannelUpdateCommand command) { - Channel channel = findById(command.id()); + public ChannelResponse update(ChannelDto dto) { + Channel channel = findById(dto.id()); // MessageResponse lastMsgResp = getLastMessageResponse(channel.getId()); - if (channel.matchChannelType(ChannelType.PRIVATE)) { - throw new APIException(ErrorCode.PRIVATE_CHANNEL_NOT_UPDATE, command.id()); - } - channel.update(command.name(), command.description()); + ensure(ChannelType.PRIVATE, + channel::matchChannelType, + type -> new APIException(ErrorCode.PRIVATE_CHANNEL_NOT_UPDATE, dto.id())); + channel.update(dto); channelRepository.save(channel); - return channel.toResponse(); + return channelMapper.toResponse(channel); } @Override public void delete(UUID id) { - if (!channelRepository.existsById(id)) { - throw new APIException(ErrorCode.CHANNELID_NOT_FOUND, id); - } - List msgToDelete = messageRepository.filter(message -> message.isInChannel(id)) - .map(Message::getId) - .toList(); - msgToDelete.forEach(messageRepository::deleteById); - - List readStatusToDelete = readStatusRepository.filter(readStatus -> readStatus.matchChannelId(id)) - .map(ReadStatus::getId) - .toList(); - readStatusToDelete.forEach(readStatusRepository::deleteById); - + ensure(id, channelRepository::existsById, val -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, val)); + List msgToDelete = messageRepository.findAllByChannelId(id); + messageRepository.deleteAll(msgToDelete); + List readStatuses = readStatusRepository.findAllByChannelId(id); + readStatusRepository.deleteAll(readStatuses); channelRepository.deleteById(id); } @Override protected Channel findById(UUID id) { - return findEntityById(id, channelRepository, + return getOrThrow(id, channelRepository::findById, () -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, id)); } // deprecated ? - private MessageResponse getLastMessageResponse(UUID channelId) { - return messageRepository.filter(message -> message.isInChannel(channelId)) - .max(Message::compareTo) - .orElseThrow(() -> new NoSuchElementException("this channel have no message")) - .toResponse(); - } +// private MessageDto getLastMessageResponse(UUID channelId) { +// return messageRepository.filter(message -> message.isInChannel(channelId)) +// .max(Message::compareTo) +// .orElseThrow(() -> new NoSuchElementException("this channel have no message")) +// .toResponse(); +// } } From c183c0bf40ca2dc98e7c6e184d5c083db3b9c6a7 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 9 Mar 2026 15:37:45 +0900 Subject: [PATCH 14/28] feat(Message): update entity, dto, mapper, service, controller - Message: add update, constructor - MessageServiceDTO: split by requestDto and dto - MessageMapper: init - BasicMessageService: update using mapper, dto - MessageController: mapper used to convert several parameter into one dto --- .../controller/MessageController.java | 21 +++--- .../discodeit/dto/MessageServiceDTO.java | 43 ------------ .../dto/message/MessageServiceDTO.java | 43 ++++++++++++ .../message/request/MessageCreateRequest.java | 8 +++ .../message/request/MessageUpdateRequest.java | 7 ++ .../mission/discodeit/entity/Message.java | 21 +++++- .../discodeit/mapper/MessageMapper.java | 33 ++++++++++ .../repository/MessageRepository.java | 2 + .../discodeit/service/MessageService.java | 8 ++- .../service/basic/BasicDomainService.java | 2 +- .../service/basic/BasicMessageService.java | 65 ++++++++++--------- 11 files changed, 163 insertions(+), 90 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageCreateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageUpdateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java diff --git a/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java b/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java index b1aca219f..8b68d1682 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java @@ -1,6 +1,10 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.MessageServiceDTO.*; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageDto; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageResponse; +import com.sprint.mission.discodeit.dto.message.request.MessageCreateRequest; +import com.sprint.mission.discodeit.dto.message.request.MessageUpdateRequest; +import com.sprint.mission.discodeit.mapper.MessageMapper; import com.sprint.mission.discodeit.service.MessageService; import jakarta.annotation.Nullable; import jakarta.validation.Valid; @@ -12,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile; import java.util.List; +import java.util.Optional; import java.util.UUID; @RestController @@ -19,15 +24,13 @@ @RequiredArgsConstructor public class MessageController { private final MessageService messageService; + private final MessageMapper messageMapper; @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity create(@RequestPart @Valid MessageCreateRequest messageCreateRequest, @RequestPart(required = false) @Nullable List attachments) { - if (attachments == null) { - attachments = List.of(); - } - MessageCreateCommand command = new MessageCreateCommand(messageCreateRequest, attachments); - return ResponseEntity.status(HttpStatus.CREATED).body(messageService.create(command)); + MessageDto dto = messageMapper.toDtoFromRequest(messageCreateRequest, Optional.ofNullable(attachments).orElse(List.of())); + return ResponseEntity.status(HttpStatus.CREATED).body(messageService.create(dto)); } @GetMapping @@ -37,9 +40,9 @@ public ResponseEntity> findInChannel(@RequestParam UUID ch @PatchMapping(value = "/{messageId}") public ResponseEntity update(@PathVariable UUID messageId, - @RequestBody MessageUpdateRequest request) { - MessageUpdateCommand command = new MessageUpdateCommand(messageId, request); - return ResponseEntity.status(HttpStatus.OK).body(messageService.update(command)); + @RequestBody @Valid MessageUpdateRequest request) { + MessageDto dto = messageMapper.toDtoFromRequest(messageId, request); + return ResponseEntity.status(HttpStatus.OK).body(messageService.update(dto)); } @DeleteMapping(value = "/{messageId}") diff --git a/src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java deleted file mode 100644 index ddc671d80..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/MessageServiceDTO.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; -import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; -import lombok.Builder; -import org.springframework.web.multipart.MultipartFile; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; - -public interface MessageServiceDTO { - record MessageCreateRequest(@Nonnull UUID authorId, @Nonnull UUID channelId, @Nonnull String content) { - } - - record MessageCreateCommand(UUID authorId, UUID channelId, String content, - List attachments) { - public MessageCreateCommand(MessageCreateRequest request, List attachments) { - this(request.authorId(), request.channelId(), request.content(), - attachments.stream().map(BinaryContentCreateRequest::from).toList()); - } - } - - record MessageUpdateRequest(@JsonProperty("newContent") String content) { - } - - record MessageUpdateCommand(UUID id, String content) { - public MessageUpdateCommand(UUID id, MessageUpdateRequest request) { - this(id, request.content()); - } - } - - // todo: error log - @Builder - record MessageDto(UUID id, UserDto author, UUID channelId, String content, - List attachments, LocalDateTime createdAt, - LocalDateTime updatedAt) { - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java new file mode 100644 index 000000000..b00a0eaed --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java @@ -0,0 +1,43 @@ +package com.sprint.mission.discodeit.dto.message; + +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelResponse; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; +import lombok.Builder; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public interface MessageServiceDTO { + // todo: error log + @Builder + record MessageResponse(UUID id, UserResponse author, ChannelResponse channel, String content, + List attachments, Instant createdAt, + Instant updatedAt) { + + } + + @Builder + record MessageDto(UUID id, UUID authorId, UUID channelId, String content, + List attachments) + implements MessageCreateDto, MessageUpdateDto { + } + + interface MessageCreateDto extends AuthorAndChannelId { + String content(); + List attachments(); + } + + interface MessageUpdateDto extends AuthorAndChannelId { + UUID id(); + String content(); + List attachments(); + } + + interface AuthorAndChannelId { + UUID authorId(); + UUID channelId(); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageCreateRequest.java new file mode 100644 index 000000000..b5e753fd3 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageCreateRequest.java @@ -0,0 +1,8 @@ +package com.sprint.mission.discodeit.dto.message.request; + +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +public record MessageCreateRequest(@NotNull UUID authorId, @NotNull UUID channelId, @NotNull String content) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageUpdateRequest.java new file mode 100644 index 000000000..df4af5dfa --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/message/request/MessageUpdateRequest.java @@ -0,0 +1,7 @@ +package com.sprint.mission.discodeit.dto.message.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; + +public record MessageUpdateRequest(@JsonProperty("newContent") @NotNull String content) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Message.java b/src/main/java/com/sprint/mission/discodeit/entity/Message.java index 866e91ab7..8a17435e0 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Message.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Message.java @@ -1,16 +1,20 @@ package com.sprint.mission.discodeit.entity; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageUpdateDto; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.List; @Getter +@NoArgsConstructor @Entity @Table(name = "messages") -public class Message extends BaseUpdatableEntity { +public class Message extends BaseUpdatableEntity { @Column(nullable = false) private String content; @@ -20,11 +24,24 @@ public class Message extends BaseUpdatableEntity { @ManyToOne @JoinColumn(name = "author_id") - private User user; + private User author; @OneToMany private List attachments = new ArrayList<>(); + @Builder + public Message(String content, Channel channel, User author, List attachments) { + this.content = content; + this.channel = channel; + this.author = author; + this.attachments = attachments; + } + + @Override + public void update(MessageUpdateDto dto) { + updateIfChanged(content, dto.content(), val -> content = val); + } + public void addAttachment(BinaryContent attachment) { this.attachments.add(attachment); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java new file mode 100644 index 000000000..4c5d439aa --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java @@ -0,0 +1,33 @@ +package com.sprint.mission.discodeit.mapper; + +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageDto; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageResponse; +import com.sprint.mission.discodeit.dto.message.request.MessageCreateRequest; +import com.sprint.mission.discodeit.dto.message.request.MessageUpdateRequest; +import com.sprint.mission.discodeit.entity.Message; +import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.UUID; + +@Mapper(config = GlobalMapperConfig.class) +public interface MessageMapper extends BaseMapper { + UserMapper userMapper = Mappers.getMapper(UserMapper.class); + BinaryContentMapper attachmentMapper = Mappers.getMapper(BinaryContentMapper.class); + + default MessageDto toDtoFromRequest(MessageCreateRequest request, List attachments) { + List attachmentDto = attachments.stream().map(attachmentMapper::toDtoFromFile).toList(); + return MessageDto.builder() + .authorId(request.authorId()) + .channelId(request.channelId()) + .content(request.content()) + .attachments(attachmentDto) + .build(); + } + + MessageDto toDtoFromRequest(UUID messageId, MessageUpdateRequest request); +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java index 7cc098cdd..986478bb0 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java @@ -3,7 +3,9 @@ import com.sprint.mission.discodeit.entity.Message; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.UUID; public interface MessageRepository extends JpaRepository { + List findAllByChannelId(UUID channelId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/MessageService.java b/src/main/java/com/sprint/mission/discodeit/service/MessageService.java index 9576a9530..457a2cec0 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/MessageService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/MessageService.java @@ -1,6 +1,8 @@ package com.sprint.mission.discodeit.service; -import com.sprint.mission.discodeit.dto.MessageServiceDTO.*; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageCreateDto; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageResponse; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageUpdateDto; import java.util.List; import java.util.UUID; @@ -8,9 +10,9 @@ public interface MessageService { List findAllByChannelId(UUID channelId); - MessageResponse create(MessageCreateCommand command); + MessageResponse create(MessageCreateDto dto); - MessageResponse update(MessageUpdateCommand command); + MessageResponse update(MessageUpdateDto dto); void delete(UUID id); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java index f4a1febaf..ab96bb109 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java @@ -10,7 +10,7 @@ public abstract class BasicDomainService { - protected T getOrThrow(R value, Function> action, Supplier exception) { + protected U getOrThrow(R value, Function> action, Supplier exception) { return action.apply(value).orElseThrow(exception); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java index 88dd117cf..65a455d29 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java @@ -2,11 +2,15 @@ import com.sprint.mission.discodeit.common.exception.code.ErrorCode; import com.sprint.mission.discodeit.common.exception.custom.APIException; -import com.sprint.mission.discodeit.dto.MessageServiceDTO.MessageCreateCommand; -import com.sprint.mission.discodeit.dto.MessageServiceDTO.MessageResponse; -import com.sprint.mission.discodeit.dto.MessageServiceDTO.MessageUpdateCommand; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.AuthorAndChannelId; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageCreateDto; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageResponse; +import com.sprint.mission.discodeit.dto.message.MessageServiceDTO.MessageUpdateDto; import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.entity.Message; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.mapper.MessageMapper; import com.sprint.mission.discodeit.repository.BinaryContentRepository; import com.sprint.mission.discodeit.repository.ChannelRepository; import com.sprint.mission.discodeit.repository.MessageRepository; @@ -14,7 +18,6 @@ import com.sprint.mission.discodeit.service.MessageService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.util.IdGenerator; import java.util.List; import java.util.UUID; @@ -26,58 +29,56 @@ public class BasicMessageService extends BasicDomainService implements private final ChannelRepository channelRepository; private final UserRepository userRepository; private final BinaryContentRepository attachmentRepository; - private final IdGenerator idGenerator; + private final MessageMapper messageMapper; @Override - public MessageResponse create(MessageCreateCommand command) { - validateUserAndChannelExist(command); - - List attachmentIds = command.attachments().stream() + public MessageResponse create(MessageCreateDto dto) { + ensureUserAndChannelExist(dto); + List attachments = dto.attachments().stream() .map(BinaryContent::new) .map(attachmentRepository::save) - .map(BinaryContent::getId) .toList(); - Message message = new Message(idGenerator.generateId(), command.content(), command.channelId(), - command.authorId(), attachmentIds); + User user = getOrThrow(dto.authorId(), userRepository::findById, () -> new APIException(ErrorCode.USERID_NOT_FOUND, dto.authorId())); + Channel channel = getOrThrow(dto.channelId(), channelRepository::findById, () -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, dto.channelId())); + Message message = Message.builder() + .content(dto.content()) + .author(user) + .channel(channel) + .attachments(attachments) + .build(); messageRepository.save(message); - return message.toResponse(); + return messageMapper.toResponse(message); } @Override public List findAllByChannelId(UUID channelId) { - return messageRepository.filter(message -> message.isInChannel(channelId)) - .map(Message::toResponse) + return messageRepository.findAllByChannelId(channelId) + .stream() + .map(messageMapper::toResponse) .toList(); } @Override - public MessageResponse update(MessageUpdateCommand command) { - Message message = findById(command.id()); - message.update(command.content(), List.of()); + public MessageResponse update(MessageUpdateDto dto) { + Message message = findById(dto.id()); + message.update(dto); messageRepository.save(message); - return message.toResponse(); + return messageMapper.toResponse(message); } @Override - public void delete(UUID messageId) { - findById(messageId).toResponse() - .attachmentIds() - .forEach(attachmentRepository::deleteById); - messageRepository.deleteById(messageId); + public void delete(UUID id) { + deleteByIdOrThrow(id, messageRepository, new APIException(ErrorCode.MESSAGEID_NOT_FOUND, id)); } @Override protected Message findById(UUID id) { - return findEntityById(id, messageRepository, + return getOrThrow(id, messageRepository::findById, () -> new APIException(ErrorCode.MESSAGEID_NOT_FOUND, id)); } - private void validateUserAndChannelExist(MessageCreateCommand command) { - if (!channelRepository.existsById(command.channelId())) { - throw new APIException(ErrorCode.CHANNELID_NOT_FOUND, command.channelId()); - } - if (!userRepository.existsById(command.authorId())) { - throw new APIException(ErrorCode.USERID_NOT_FOUND, command.authorId()); - } + private void ensureUserAndChannelExist(AuthorAndChannelId dto) { + ensure(dto.authorId(), userRepository::existsById, id -> new APIException(ErrorCode.USERID_NOT_FOUND, id)); + ensure(dto.channelId(), channelRepository::existsById, id -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, id)); } } From 92be7eb893d008631ada62afd4aedfb20a470cb0 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 9 Mar 2026 16:30:40 +0900 Subject: [PATCH 15/28] feat(BinaryContent): update entity, dto, mapper, service, controller - BinaryContent: add constructor - BinaryContentServiceDto: init requestDto, dto - BinaryContentMapper: init - BasicBinaryContentService: using mapper - BinaryContentController: --- .../discodeit/dto/BinaryContentServiceDTO.java | 16 ---------------- .../binarycontent/BinaryContentServiceDTO.java | 4 +++- .../request/BinaryContentCreateRequest.java | 14 +------------- .../mission/discodeit/entity/BinaryContent.java | 16 ++++++++++++++++ .../discodeit/mapper/BinaryContentMapper.java | 17 +++++++++++++++++ .../discodeit/service/BinaryContentService.java | 8 ++++---- .../basic/BasicBinaryContentService.java | 16 ++++++++-------- 7 files changed, 49 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/BinaryContentServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java diff --git a/src/main/java/com/sprint/mission/discodeit/dto/BinaryContentServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/BinaryContentServiceDTO.java deleted file mode 100644 index 3c2587f0d..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/BinaryContentServiceDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import lombok.Builder; -import lombok.NonNull; - -import java.util.UUID; - -public interface BinaryContentServiceDTO { - record BinaryContentCreateRequest(@NonNull String fileName, @NonNull String fileType, @NonNull byte[] data) { - } - - @Builder - record BinaryContentResponse(@NonNull UUID id, @NonNull String fileName, @NonNull String fileType, - @NonNull byte[] data) { - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java index c9e70d14d..803cedb38 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java @@ -5,8 +5,10 @@ import java.util.UUID; public interface BinaryContentServiceDTO { + record BinaryContentResponse(UUID id, String fileName, Long size, String contentType) { + } @Builder - record BinaryContentDto(UUID id, String fileName, int size, String contentType) { + record BinaryContentDto(UUID id, String fileName, Long size, String contentType, byte[] bytes) { } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/request/BinaryContentCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/request/BinaryContentCreateRequest.java index 3af2c6674..53feabb2e 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/request/BinaryContentCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/request/BinaryContentCreateRequest.java @@ -1,16 +1,4 @@ package com.sprint.mission.discodeit.dto.binarycontent.request; -import jakarta.validation.constraints.NotEmpty; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; - -public record BinaryContentCreateRequest(@NotEmpty String fileName, byte[] data) { - public static BinaryContentCreateRequest from(MultipartFile file) { - try { - return new BinaryContentCreateRequest(file.getOriginalFilename(), file.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } +public record BinaryContentCreateRequest(String fileName, byte[] bytes, String contentType, Long size) { } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java index 7af3e504f..468bc7660 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java @@ -1,12 +1,16 @@ package com.sprint.mission.discodeit.entity; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; import com.sprint.mission.discodeit.entity.base.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor @Entity @Table(name = "binary_contents") public class BinaryContent extends BaseEntity { @@ -21,4 +25,16 @@ public class BinaryContent extends BaseEntity { @Column(nullable = false) private byte[] bytes; + + @Builder + public BinaryContent(String fileName, Long size, String contentType, byte[] bytes) { + this.fileName = fileName; + this.size = size; + this.contentType = contentType; + this.bytes = bytes; + } + + public BinaryContent(BinaryContentDto dto) { + this(dto.fileName(), dto.size(), dto.contentType(), dto.bytes()); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java new file mode 100644 index 000000000..38cf3bcb1 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.mapper; + +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; +import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; +import org.mapstruct.Mapper; +import org.springframework.web.multipart.MultipartFile; + +@Mapper(config = GlobalMapperConfig.class) +public interface BinaryContentMapper extends BaseMapper { + // todo: add 'read file error' + BinaryContentDto toDtoFromFile(MultipartFile file); + + BinaryContent toEntityFromRequest(BinaryContentCreateRequest request); +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java b/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java index 73a337760..74110f9b5 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java @@ -1,17 +1,17 @@ package com.sprint.mission.discodeit.service; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO; import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; -import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; import java.util.List; import java.util.UUID; public interface BinaryContentService { - BinaryContentResponse create(BinaryContentCreateRequest request); + BinaryContentServiceDTO.BinaryContentResponse create(BinaryContentCreateRequest request); - BinaryContentResponse find(UUID id); + BinaryContentServiceDTO.BinaryContentResponse find(UUID id); void delete(UUID id); - List findAllByIdIn(List ids); + List findAllByIdIn(List ids); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java index f1f3416de..54ea731b8 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java @@ -2,51 +2,51 @@ import com.sprint.mission.discodeit.common.exception.code.ErrorCode; import com.sprint.mission.discodeit.common.exception.custom.APIException; -import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; +import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.mapper.BinaryContentMapper; import com.sprint.mission.discodeit.repository.BinaryContentRepository; import com.sprint.mission.discodeit.service.BinaryContentService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @Service @RequiredArgsConstructor public class BasicBinaryContentService extends BasicDomainService implements BinaryContentService { private final BinaryContentRepository binaryContentRepository; + private final BinaryContentMapper binaryContentMapper; @Override public List findAllByIdIn(List ids) { return ids.stream() - .filter(binaryContentRepository::existsById) .map(this::find) .toList(); } @Override public BinaryContentResponse create(BinaryContentCreateRequest request) { - BinaryContent content = new BinaryContent(request.fileName(), request.data()); + BinaryContent content = binaryContentMapper.toEntityFromRequest(request); binaryContentRepository.save(content); - return content.toResponse(); + return binaryContentMapper.toResponse(content); } @Override public BinaryContentResponse find(UUID id) { - return findById(id).toResponse(); + return binaryContentMapper.toResponse(findById(id)); } @Override public void delete(UUID id) { - deleteIfExist(id, binaryContentRepository, () -> new APIException(ErrorCode.BINARYCONTENTID_NOT_FOUND, id)); + deleteByIdOrThrow(id, binaryContentRepository, new APIException(ErrorCode.BINARYCONTENTID_NOT_FOUND, id)); } @Override protected BinaryContent findById(UUID id) { - return findEntityById(id, binaryContentRepository, + return getOrThrow(id, binaryContentRepository::findById, () -> new APIException(ErrorCode.BINARYCONTENTID_NOT_FOUND, id)); } } From 12d4e8fb6b845916d03decb7ea1b053363ed87ac Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 9 Mar 2026 20:40:00 +0900 Subject: [PATCH 16/28] feat(ReadStatus): update entity, dto, repository, service, controller and init mapper - ReadStatus: add constructor, update - ReadStatusServiceDTO: split by requestDto and dto - ReadStatusMapper: init - ReadStatusRepository: add features about userId and channelId - BasicReadStatusService: update using mapper - ReadStatusController: convert requestDto to ReadStatusDto --- .../controller/ReadStatusController.java | 24 ++++--- .../discodeit/dto/ReadStatusServiceDTO.java | 26 ------- .../dto/readstatus/ReadStatusServiceDTO.java | 33 +++++++++ .../request/ReadStatusCreateRequest.java | 12 ++++ .../request/ReadStatusUpdateRequest.java | 9 +++ .../mission/discodeit/entity/ReadStatus.java | 16 ++++- .../discodeit/mapper/ReadStatusMapper.java | 11 +++ .../repository/ReadStatusRepository.java | 13 ++++ .../discodeit/service/ReadStatusService.java | 10 +-- .../service/basic/BasicReadStatusService.java | 67 +++++++++---------- 10 files changed, 146 insertions(+), 75 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/readstatus/ReadStatusServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java diff --git a/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java b/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java index f98f9def1..60f05f10e 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java @@ -1,10 +1,10 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusCreateRequest; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusResponse; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusUpdateCommand; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusUpdateRequest; +import com.sprint.mission.discodeit.common.util.TimeConverter; +import com.sprint.mission.discodeit.dto.readstatus.request.ReadStatusCreateRequest; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusDto; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusResponse; +import com.sprint.mission.discodeit.dto.readstatus.request.ReadStatusUpdateRequest; import com.sprint.mission.discodeit.service.ReadStatusService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -28,14 +28,22 @@ public ResponseEntity> find(@RequestParam UUID userId) @PostMapping public ResponseEntity create(@RequestBody @Valid ReadStatusCreateRequest request) { - return ResponseEntity.status(HttpStatus.CREATED).body(readStatusService.create(request)); + ReadStatusDto dto = ReadStatusDto.builder() + .userId(request.userId()) + .channelId(request.channelId()) + .lastReadAt(TimeConverter.toInstant(request.lastReadAt())) + .build(); + return ResponseEntity.status(HttpStatus.CREATED).body(readStatusService.create(dto)); } @PatchMapping(value = "/{readStatusId}") public ResponseEntity update(@PathVariable UUID readStatusId, @RequestBody @Valid ReadStatusUpdateRequest request) { - ReadStatusUpdateCommand command = new ReadStatusUpdateCommand(readStatusId, request.newLastReadAt()); - return ResponseEntity.status(HttpStatus.OK).body(readStatusService.update(command)); + ReadStatusDto dto = ReadStatusDto.builder() + .id(readStatusId) + .lastReadAt(TimeConverter.toInstant(request.newLastReadAt())) + .build(); + return ResponseEntity.status(HttpStatus.OK).body(readStatusService.update(dto)); } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java deleted file mode 100644 index 62c744bdb..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/ReadStatusServiceDTO.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import jakarta.annotation.Nonnull; -import lombok.Builder; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.LocalDateTime; -import java.util.UUID; - -public interface ReadStatusServiceDTO { - record ReadStatusCreateRequest(@Nonnull UUID userId, - @Nonnull UUID channelId, - @Nonnull @DateTimeFormat LocalDateTime lastReadAt) { - } - - record ReadStatusUpdateRequest(@Nonnull @DateTimeFormat LocalDateTime newLastReadAt) { - } - - record ReadStatusUpdateCommand(UUID id, LocalDateTime datetime) { - } - - // todo: error log - @Builder - record ReadStatusDto(UUID id, UUID userId, UUID channelId, LocalDateTime lastReadAt) { - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/readstatus/ReadStatusServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/ReadStatusServiceDTO.java new file mode 100644 index 000000000..23c05aacf --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/ReadStatusServiceDTO.java @@ -0,0 +1,33 @@ +package com.sprint.mission.discodeit.dto.readstatus; + +import lombok.Builder; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.UUID; + +public interface ReadStatusServiceDTO { + // todo: error log + @Builder + record ReadStatusResponse(UUID id, UUID userId, UUID channelId, LocalDateTime lastReadAt) { + } + + @Builder + record ReadStatusDto(UUID id, UUID userId, UUID channelId, Instant lastReadAt) + implements ReadStatusCreateDto, ReadStatusUpdateDto { + } + + interface ReadStatusCreateDto extends CreatableDto { + Instant lastReadAt(); + } + + interface ReadStatusUpdateDto { + UUID id(); + Instant lastReadAt(); + } + + interface CreatableDto { + UUID userId(); + UUID channelId(); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java new file mode 100644 index 000000000..16cc8a6a1 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java @@ -0,0 +1,12 @@ +package com.sprint.mission.discodeit.dto.readstatus.request; + +import jakarta.validation.constraints.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record ReadStatusCreateRequest(@NotNull UUID userId, + @NotNull UUID channelId, + @NotNull @DateTimeFormat LocalDateTime lastReadAt) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java new file mode 100644 index 000000000..fd6a4e9a1 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.dto.readstatus.request; + +import jakarta.validation.constraints.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +public record ReadStatusUpdateRequest(@DateTimeFormat @NotNull LocalDateTime newLastReadAt) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java index df6e98b0f..05d0bea3f 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java @@ -1,13 +1,16 @@ package com.sprint.mission.discodeit.entity; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusUpdateDto; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; +import lombok.NoArgsConstructor; import java.time.Instant; +@NoArgsConstructor @Entity @Table(name = "read_statuses") -public class ReadStatus extends BaseUpdatableEntity { +public class ReadStatus extends BaseUpdatableEntity { @ManyToOne @JoinColumn(name = "user_id") private User user; @@ -18,4 +21,15 @@ public class ReadStatus extends BaseUpdatableEntity { @Column private Instant lastReadAt; + + public ReadStatus(User user, Channel channel, Instant lastReadAt) { + this.user = user; + this.channel = channel; + this.lastReadAt = lastReadAt; + } + + @Override + public void update(ReadStatusUpdateDto dto) { + updateIfChanged(lastReadAt, dto.lastReadAt(), val -> lastReadAt = val); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java new file mode 100644 index 000000000..47bc40ced --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.mapper; + +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusDto; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusResponse; +import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; +import org.mapstruct.Mapper; + +@Mapper(config = GlobalMapperConfig.class) +public interface ReadStatusMapper extends BaseMapper { +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java index c9478c1ba..9f1f85e32 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java @@ -1,10 +1,23 @@ package com.sprint.mission.discodeit.repository; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.CreatableDto; import com.sprint.mission.discodeit.entity.ReadStatus; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; import java.util.UUID; public interface ReadStatusRepository extends JpaRepository { boolean existsByUserIdAndChannelId(UUID userId, UUID channelId); + + default boolean existsByUserIdAndChannelId(CreatableDto dto) { + return existsByUserIdAndChannelId(dto.userId(), dto.channelId()); + } + + Optional findByUserIdAndChannelId(UUID userId, UUID channelId); + + List findAllByChannelId(UUID channelId); + + List findAllByUserId(UUID userId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java index 64da643b1..9a9afced4 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java @@ -1,14 +1,14 @@ package com.sprint.mission.discodeit.service; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusCreateRequest; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusResponse; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusUpdateCommand; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusCreateDto; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusResponse; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusUpdateDto; import java.util.List; import java.util.UUID; -public interface ReadStatusService extends DomainService { +public interface ReadStatusService extends DomainService { List findAllByUserId(UUID userId); - ReadStatusResponse find(UUID channelId, UUID userId); + ReadStatusResponse find(UUID userId, UUID channelId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java index 4620896ca..33a13f04c 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java @@ -2,11 +2,14 @@ import com.sprint.mission.discodeit.common.exception.code.ErrorCode; import com.sprint.mission.discodeit.common.exception.custom.APIException; -import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusCreateRequest; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusResponse; -import com.sprint.mission.discodeit.dto.ReadStatusServiceDTO.ReadStatusUpdateCommand; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.CreatableDto; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusCreateDto; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusResponse; +import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusUpdateDto; +import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.mapper.ReadStatusMapper; import com.sprint.mission.discodeit.repository.ChannelRepository; import com.sprint.mission.discodeit.repository.ReadStatusRepository; import com.sprint.mission.discodeit.repository.UserRepository; @@ -24,66 +27,60 @@ public class BasicReadStatusService extends BasicDomainService imple private final ReadStatusRepository readStatusRepository; private final UserRepository userRepository; private final ChannelRepository channelRepository; + private final ReadStatusMapper readStatusMapper; @Override public List findAllByUserId(UUID userId) { - return readStatusRepository.filter(status -> status.matchUserId(userId)) - .map(ReadStatus::toResponse) + return readStatusRepository.findAllByUserId(userId) + .stream() + .map(readStatusMapper::toResponse) .toList(); } @Override - public ReadStatusResponse create(ReadStatusCreateRequest request) { - verifyCreatable(request); - - ReadStatus status = new ReadStatus(request.userId(), request.channelId(), - TimeConverter.toInstant(request.lastReadAt())); + public ReadStatusResponse create(ReadStatusCreateDto dto) { + verifyCreatable(dto); + User user = userRepository.getReferenceById(dto.userId()); + Channel channel = channelRepository.getReferenceById(dto.channelId()); + ReadStatus status = new ReadStatus(user, channel, dto.lastReadAt()); readStatusRepository.save(status); - return status.toResponse(); + return readStatusMapper.toResponse(status); } @Override public ReadStatusResponse find(UUID id) { - return findById(id).toResponse(); + return readStatusMapper.toResponse(findById(id)); } @Override - public ReadStatusResponse update(ReadStatusUpdateCommand command) { - ReadStatus status = findById(command.id()); - status.update(command.datetime()); + public ReadStatusResponse update(ReadStatusUpdateDto dto) { + ReadStatus status = findById(dto.id()); + status.update(dto); readStatusRepository.save(status); - return status.toResponse(); + return readStatusMapper.toResponse(status); } @Override public void delete(UUID id) { - deleteIfExist(id, readStatusRepository, () -> new APIException(ErrorCode.READSTATUSID_NOT_FOUND, id)); + deleteByIdOrThrow(id, readStatusRepository, new APIException(ErrorCode.READSTATUSID_NOT_FOUND, id)); } @Override protected ReadStatus findById(UUID id) { - return findEntityById(id, readStatusRepository, () -> new APIException(ErrorCode.READSTATUSID_NOT_FOUND, id)); + return getOrThrow(id, readStatusRepository::findById, + () -> new APIException(ErrorCode.READSTATUSID_NOT_FOUND, id)); } @Override - public ReadStatusResponse find(UUID channelId, UUID userId) { - return findByChannelAndUser(channelId, userId).toResponse(); - } - - private ReadStatus findByChannelAndUser(UUID channelId, UUID userId) { - return readStatusRepository.filter(readStatus -> readStatus.matchChannelId(channelId)) - .filter(readStatus -> readStatus.matchUserId(userId)) - .findFirst() + public ReadStatusResponse find(UUID userId, UUID channelId) { + ReadStatus status = readStatusRepository.findByUserIdAndChannelId(userId, channelId) .orElseThrow(() -> new APIException(ErrorCode.READSTATUSID_NOT_FOUND, Map.of("userId", userId, "channelId", channelId))); + return readStatusMapper.toResponse(status); } - private void verifyCreatable(ReadStatusCreateRequest request) { - ensure(() -> userRepository.existsById(request.userId()), - () -> new APIException(ErrorCode.USERID_NOT_FOUND, request.userId())); - ensure(() -> channelRepository.existsById(request.channelId()), - () -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, request.channelId())); - ensure(() -> readStatusRepository.existsByUserAndChannelId(request.userId(), request.channelId()), - () -> new APIException(ErrorCode.READSTATUS_ALREADY_EXIST, Map.of("userId", request.userId(), "channelId", request.channelId()))); + private void verifyCreatable(CreatableDto dto) { + ensure(dto.userId(), userRepository::existsById, id -> new APIException(ErrorCode.USERID_NOT_FOUND, id)); + ensure(dto.channelId(), channelRepository::existsById, id -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, id)); + ensure(dto, readStatusRepository::existsByUserIdAndChannelId, dto1 -> new APIException(ErrorCode.READSTATUS_ALREADY_EXIST, dto1)); } - } From 040023a62774dd5c0e8170313d1978208035e4c6 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Tue, 10 Mar 2026 09:13:54 +0900 Subject: [PATCH 17/28] feat(Auth): update dto, service, controller, UserRepository - AuthServiceDTO: change annotation - UserRepository: add feature for auth - BasicAuthService: update login - AuthController: add Valid annotation --- .../discodeit/controller/AuthController.java | 11 ++++++---- .../mission/discodeit/dto/AuthServiceDTO.java | 8 ------- .../discodeit/dto/UserStatusServiceDTO.java | 22 ------------------- .../discodeit/dto/auth/AuthServiceDTO.java | 8 +++++++ .../discodeit/repository/UserRepository.java | 3 +++ .../discodeit/service/AuthService.java | 2 +- .../service/basic/BasicAuthService.java | 14 ++++++------ 7 files changed, 26 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/UserStatusServiceDTO.java create mode 100644 src/main/java/com/sprint/mission/discodeit/dto/auth/AuthServiceDTO.java diff --git a/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java b/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java index 755f3c220..5905a095d 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java @@ -1,13 +1,16 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.AuthServiceDTO.LoginRequest; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO; +import com.sprint.mission.discodeit.dto.auth.AuthServiceDTO.LoginRequest; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.service.AuthService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/auth") @@ -16,7 +19,7 @@ public class AuthController { private final AuthService authService; @PostMapping(value = "/login") - public ResponseEntity login(@RequestBody LoginRequest request) { + public ResponseEntity login(@RequestBody @Valid LoginRequest request) { return ResponseEntity.status(HttpStatus.OK).body(authService.login(request)); } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java deleted file mode 100644 index 595594c68..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/AuthServiceDTO.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import jakarta.annotation.Nonnull; - -public interface AuthServiceDTO { - record LoginRequest(@Nonnull String username, @Nonnull String password) { - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/UserStatusServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/UserStatusServiceDTO.java deleted file mode 100644 index f5279404f..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/UserStatusServiceDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import lombok.NonNull; - -import java.util.UUID; - -public interface UserStatusServiceDTO { - record UserStatusCreateRequest(@NonNull UUID userId) { - } - - record UserStatusUpdateRequest(UUID id, UUID userId) { - public UserStatusUpdateRequest { - if (id == null && userId == null) { - throw new IllegalArgumentException( - "both can't be null, at least one of the two must not be null"); - } - } - } - - record UserStatusResponse(UUID id, UUID userId, boolean isActive) { - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/auth/AuthServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/auth/AuthServiceDTO.java new file mode 100644 index 000000000..a9d428f26 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/auth/AuthServiceDTO.java @@ -0,0 +1,8 @@ +package com.sprint.mission.discodeit.dto.auth; + +import jakarta.validation.constraints.NotNull; + +public interface AuthServiceDTO { + record LoginRequest(@NotNull String username, @NotNull String password) { + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java index 40df87501..37440e3f6 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java @@ -3,10 +3,13 @@ import com.sprint.mission.discodeit.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { boolean existsByUsername(String username); boolean existsByEmail(String email); + + Optional findByUsernameAndPassword(String username, String password); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/AuthService.java b/src/main/java/com/sprint/mission/discodeit/service/AuthService.java index 64b3c84a8..d676ac614 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/AuthService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/AuthService.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.service; -import com.sprint.mission.discodeit.dto.AuthServiceDTO.LoginRequest; +import com.sprint.mission.discodeit.dto.auth.AuthServiceDTO.LoginRequest; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; public interface AuthService { diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java index b1bbbef79..513bf6604 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java @@ -2,8 +2,10 @@ import com.sprint.mission.discodeit.common.exception.code.ErrorCode; import com.sprint.mission.discodeit.common.exception.custom.APIException; -import com.sprint.mission.discodeit.dto.AuthServiceDTO.LoginRequest; +import com.sprint.mission.discodeit.dto.auth.AuthServiceDTO.LoginRequest; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.mapper.UserMapper; import com.sprint.mission.discodeit.repository.UserRepository; import com.sprint.mission.discodeit.service.AuthService; import lombok.RequiredArgsConstructor; @@ -13,14 +15,12 @@ @RequiredArgsConstructor public class BasicAuthService implements AuthService { private final UserRepository userRepository; + private final UserMapper userMapper; @Override public UserResponse login(LoginRequest request) { - return userRepository.filter(user -> user.matchUsername(request.username())) - .filter(user -> user.matchPassword(request.password())) - .findFirst() - .orElseThrow(() -> new APIException(ErrorCode.USERNAME_OR_PASSWORD_INCORRECT)) - .toResponse(true); - + User user = userRepository.findByUsernameAndPassword(request.username(), request.password()) + .orElseThrow(() -> new APIException(ErrorCode.USERNAME_OR_PASSWORD_INCORRECT, request)); + return userMapper.toResponse(user); } } From af182bb76dabfddc1ba5dc86264ff76fc047156e Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Tue, 10 Mar 2026 11:05:19 +0900 Subject: [PATCH 18/28] feat(UserStatus): remove useless features and init mapper and update entity - BasicUserStatusService: remove create, find, findAll, delete - UserStatusMapper: init - UserStatus: add constructor, update - UserStatusServiceDTO: update --- .../dto/userstatus/UserStatusServiceDTO.java | 9 ++- .../command/UserStatusUpdateCommand.java | 7 --- .../request/UserStatusCreateRequest.java | 8 --- .../request/UserStatusUpdateRequest.java | 3 +- .../mission/discodeit/entity/UserStatus.java | 25 +++++++- .../discodeit/mapper/UserStatusMapper.java | 15 +++++ .../discodeit/service/UserStatusService.java | 9 +-- .../service/basic/BasicUserStatusService.java | 57 +++---------------- 8 files changed, 58 insertions(+), 75 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/userstatus/command/UserStatusUpdateCommand.java delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusCreateRequest.java create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java diff --git a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java index 1a8ec5412..1e1395190 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java @@ -1,13 +1,16 @@ package com.sprint.mission.discodeit.dto.userstatus; -import lombok.Builder; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; +import java.time.Instant; import java.time.LocalDateTime; import java.util.UUID; public interface UserStatusServiceDTO { + record UserStatusResponse(UUID id, UserResponse user, LocalDateTime lastActiveAt) { + } - @Builder - record UserStatusDto(UUID id, UUID userId, LocalDateTime lastActiveAt) { + record UserStatusDto(UUID id, UUID userId, Instant lastActiveAt) { } + } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/command/UserStatusUpdateCommand.java b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/command/UserStatusUpdateCommand.java deleted file mode 100644 index 2c3b2652c..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/command/UserStatusUpdateCommand.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sprint.mission.discodeit.dto.userstatus.command; - -import java.time.LocalDateTime; -import java.util.UUID; - -public record UserStatusUpdateCommand(UUID userId, LocalDateTime datetime) { -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusCreateRequest.java deleted file mode 100644 index f01e1b288..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusCreateRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.sprint.mission.discodeit.dto.userstatus.request; - -import lombok.NonNull; - -import java.util.UUID; - -public record UserStatusCreateRequest(@NonNull UUID userId) { -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java index 3fbeffb3f..70cf20c16 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java @@ -1,9 +1,10 @@ package com.sprint.mission.discodeit.dto.userstatus.request; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -public record UserStatusUpdateRequest(@JsonProperty("newLastActiveAt") @DateTimeFormat LocalDateTime datetime) { +public record UserStatusUpdateRequest(@NotNull @JsonProperty("newLastActiveAt") @DateTimeFormat LocalDateTime datetime) { } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java index 0dc616e7c..7f3bd5d62 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java @@ -1,19 +1,42 @@ package com.sprint.mission.discodeit.entity; +import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusDto; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import java.time.Duration; import java.time.Instant; +@NoArgsConstructor @Getter @Entity @Table(name = "user_statuses") -public class UserStatus extends BaseUpdatableEntity { +public class UserStatus extends BaseUpdatableEntity { + private static final Long ACTIVE_THRESHOLD = 300L; + + @Setter @OneToOne @JoinColumn(name = "user_id") private User user; @Column private Instant lastActiveAt; + + public UserStatus(User user, Instant lastActiveAt) { + this.user = user; + this.lastActiveAt = lastActiveAt; + user.setStatus(this); + } + + public boolean isOnline() { + return Duration.between(lastActiveAt, Instant.now()).getSeconds() < ACTIVE_THRESHOLD; + } + + @Override + public void update(UserStatusDto updateDto) { + updateIfChanged(lastActiveAt, updateDto.lastActiveAt(), val -> lastActiveAt = val); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java new file mode 100644 index 000000000..03ea9cfee --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java @@ -0,0 +1,15 @@ +package com.sprint.mission.discodeit.mapper; + +import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusDto; +import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusResponse; +import com.sprint.mission.discodeit.dto.userstatus.request.UserStatusUpdateRequest; +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; +import org.mapstruct.Mapper; + +import java.util.UUID; + +@Mapper(config = GlobalMapperConfig.class) +public interface UserStatusMapper extends BaseMapper { + UserStatusDto toEntity(UUID userId, UserStatusUpdateRequest request); +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java index 57461774d..f23969a3c 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java @@ -1,11 +1,8 @@ package com.sprint.mission.discodeit.service; -import com.sprint.mission.discodeit.dto.userstatus.request.UserStatusCreateRequest; +import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusDto; import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusResponse; -import com.sprint.mission.discodeit.dto.userstatus.command.UserStatusUpdateCommand; -import java.util.List; - -public interface UserStatusService extends DomainService { - List findAll(); +public interface UserStatusService { + UserStatusResponse update(UserStatusDto dto); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java index f758d0fc2..216a06942 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java @@ -2,75 +2,34 @@ import com.sprint.mission.discodeit.common.exception.code.ErrorCode; import com.sprint.mission.discodeit.common.exception.custom.APIException; +import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusDto; import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusResponse; -import com.sprint.mission.discodeit.dto.userstatus.command.UserStatusUpdateCommand; -import com.sprint.mission.discodeit.dto.userstatus.request.UserStatusCreateRequest; -import com.sprint.mission.discodeit.entity.User; import com.sprint.mission.discodeit.entity.UserStatus; -import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.mapper.UserStatusMapper; import com.sprint.mission.discodeit.repository.UserStatusRepository; import com.sprint.mission.discodeit.service.UserStatusService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; import java.util.UUID; @Service @RequiredArgsConstructor public class BasicUserStatusService extends BasicDomainService implements UserStatusService { private final UserStatusRepository userStatusRepository; - private final UserRepository userRepository; + private final UserStatusMapper userStatusMapper; @Override - public UserStatusResponse create(UserStatusCreateRequest request) { - User user = findUser(request.userId()); - if (userStatusRepository.existsByUserId(user.getId())) { - throw new APIException(ErrorCode.USERSTATUS_ALREADY_EXIST, request.userId()); - } - UserStatus status = new UserStatus(user.getId()); + public UserStatusResponse update(UserStatusDto dto) { + UserStatus status = findById(dto.id()); + status.update(dto); userStatusRepository.save(status); - return status.toResponse(); - } - - @Override - public UserStatusResponse find(UUID id) { - return findById(id).toResponse(); - } - - @Override - public List findAll() { - return userStatusRepository.streamAll(stream -> stream.map(UserStatus::toResponse)) - .toList(); - } - - @Override - public UserStatusResponse update(UserStatusUpdateCommand command) { - UserStatus status = findByUserId(command.userId()); - status.update(command.datetime()); - userStatusRepository.save(status); - return status.toResponse(); - } - - @Override - public void delete(UUID id) { - deleteIfExist(id, userStatusRepository, - () -> new APIException(ErrorCode.USERSTATUSID_NOT_FOUND, id)); + return userStatusMapper.toResponse(status); } @Override protected UserStatus findById(UUID id) { - return findEntityById(id, userStatusRepository, + return getOrThrow(id, userStatusRepository::findById, () -> new APIException(ErrorCode.USERSTATUSID_NOT_FOUND, id)); } - - private UserStatus findByUserId(UUID userId) { - return userStatusRepository.findByUserId(userId) - .orElseThrow(() -> new APIException(ErrorCode.USERSTATUS_NOT_FOUND_BY_USERID, userId)); - } - - private User findUser(UUID userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new APIException(ErrorCode.USERID_NOT_FOUND, userId)); - } } From dd2ab0c04cee8b3b887f677bce0e3b91a3fc3339 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Tue, 10 Mar 2026 23:57:48 +0900 Subject: [PATCH 19/28] feat(storage): init BinaryContentStorage and record class - BinaryContentController: add download feature --- .../common/exception/code/ErrorCode.java | 5 + .../controller/BinaryContentController.java | 8 ++ .../storage/BinaryContentStorage.java | 13 +++ .../storage/LocalBCStorageProperties.java | 9 ++ .../storage/LocalBinaryContentStorage.java | 91 +++++++++++++++++++ src/main/resources/application-dev.yml | 24 +++-- 6 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java create mode 100644 src/main/java/com/sprint/mission/discodeit/storage/LocalBCStorageProperties.java create mode 100644 src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java diff --git a/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java b/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java index ce54f8871..914f7a05a 100644 --- a/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java +++ b/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java @@ -8,6 +8,11 @@ public enum ErrorCode { // common error INTERNAL_SERVER_ERROR(500, "C001", "Internal server error"), + FILE_CANT_READ(500, "C002", "Failed to read file"), + ROOT_DIRECTORY_FAILED_TO_CREATE(500, "C003", "Failed to create root directories"), + FILE_CANT_WRITE(500, "C004", "Failed to save file"), + FILE_ALREADY_EXIST(500, "C005", "File already exists"), + FILE_NOT_FOUND(500, "C006", "File not found"), // error related to user USERID_NOT_FOUND(404, "U001", "User id not found"), diff --git a/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java b/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java index 639c33552..252ee10a4 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; import com.sprint.mission.discodeit.service.BinaryContentService; +import com.sprint.mission.discodeit.storage.BinaryContentStorage; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,6 +16,7 @@ @RequiredArgsConstructor class BinaryContentController { private final BinaryContentService binaryContentService; + private final BinaryContentStorage binaryContentStorage; @GetMapping(value = "/{binaryContentId}") public ResponseEntity find(@PathVariable UUID binaryContentId) { @@ -25,4 +27,10 @@ public ResponseEntity find(@PathVariable UUID binaryConte public ResponseEntity> findMany(@RequestParam List binaryContentIds) { return ResponseEntity.status(HttpStatus.OK).body(binaryContentService.findAllByIdIn(binaryContentIds)); } + + @GetMapping(value = "/{binaryContentId}/download") + public ResponseEntity download(@PathVariable UUID binaryContentId) { + BinaryContentResponse dto = binaryContentService.find(binaryContentId); + return binaryContentStorage.download(dto); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java b/src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java new file mode 100644 index 000000000..ecce1da6e --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.storage; + +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; +import org.springframework.http.ResponseEntity; + +import java.io.InputStream; +import java.util.UUID; + +public interface BinaryContentStorage { + UUID put(UUID id, byte[] bytes); + InputStream get(UUID id); + ResponseEntity download(BinaryContentResponse dto); +} diff --git a/src/main/java/com/sprint/mission/discodeit/storage/LocalBCStorageProperties.java b/src/main/java/com/sprint/mission/discodeit/storage/LocalBCStorageProperties.java new file mode 100644 index 000000000..9037e9f66 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/storage/LocalBCStorageProperties.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.storage; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.nio.file.Path; + +@ConfigurationProperties(prefix = "discodeit.storage.local") +public record LocalBCStorageProperties(Path rootPath) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java b/src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java new file mode 100644 index 000000000..246c456e9 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java @@ -0,0 +1,91 @@ +package com.sprint.mission.discodeit.storage; + +import com.sprint.mission.discodeit.common.exception.code.ErrorCode; +import com.sprint.mission.discodeit.common.exception.custom.APIException; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; +import jakarta.annotation.PostConstruct; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +@Component +@ConditionalOnProperty( + prefix = "discodeit.storage", + name = "type", + havingValue = "local" +) +@EnableConfigurationProperties(LocalBCStorageProperties.class) +public class LocalBinaryContentStorage implements BinaryContentStorage { + private final Path root; + + public LocalBinaryContentStorage(LocalBCStorageProperties properties) { + this.root = properties.rootPath(); + } + + @Override + public UUID put(UUID id, byte[] bytes) { + Path path = resolvePath(id); + if (isPresent(path)) { + throw new APIException(ErrorCode.FILE_ALREADY_EXIST, path); + } + try { + Files.write(path, bytes); + return id; + } catch (IOException e) { + throw new APIException(ErrorCode.FILE_CANT_WRITE, e.getMessage()); + } + } + + @Override + public InputStream get(UUID id) { + Path path = resolvePath(id); + if (!isPresent(path)) { + throw new APIException(ErrorCode.FILE_NOT_FOUND, path); + } + try { + return Files.newInputStream(path); + } catch (IOException e) { + throw new APIException(ErrorCode.FILE_CANT_READ, e.getMessage()); + } + } + + @Override + public ResponseEntity download(BinaryContentResponse dto) { + try (InputStream inputStream = get(dto.id())) { + return ResponseEntity.status(HttpStatus.OK) + .contentLength(dto.size()) + .contentType(MediaType.IMAGE_JPEG) + .body(new InputStreamResource(inputStream)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @PostConstruct + public void init() { + try { + Files.createDirectories(root); + } catch (IOException e) { + throw new APIException(ErrorCode.ROOT_DIRECTORY_FAILED_TO_CREATE, root); + } + } + + private Path resolvePath(UUID id) { + return root.resolve(id.toString()); + } + + private boolean isPresent(Path path) { + return path.toFile().exists(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 86051c2c2..df9f1355d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,9 +1,8 @@ discodeit: - repository: - type: jcf # jcf | file - file-directory: .discodeit - uuid: - type: dev # dev / prod + storage: + type: local # local or db + local: + root-path: /binary-contents springdoc: swagger-ui: @@ -25,4 +24,17 @@ spring: ddl-auto: none show-sql: true database: PostgreSQL - defer-datasource-initialization: false \ No newline at end of file + defer-datasource-initialization: false + properties: + hibernate: + format_sql: true + +logging: + level: + org: + hibernate: + SQL: debug + orm: + jdbc: + bind: trace + extract: trace From 73b787f9e89cf7502d7ee48d6bad2c7fcea84ec8 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Wed, 11 Mar 2026 10:39:17 +0900 Subject: [PATCH 20/28] feat(User): update entity, dto, mapper, service, controller - User: add constructor - UserDto: add constructor - UserFindRequest: removed - UserMapper: update toResponse, remove toDtoFrom*Request - UserService: remove extends DomainService - BasicUserService: add transaction - UserController: remove mappers --- .../discodeit/controller/UserController.java | 20 ++++++++--------- .../discodeit/dto/user/UserServiceDTO.java | 15 ++++++++++++- .../dto/user/request/UserCreateRequest.java | 8 +++---- .../dto/user/request/UserFindRequest.java | 6 ----- .../sprint/mission/discodeit/entity/User.java | 9 ++++++-- .../mission/discodeit/mapper/UserMapper.java | 22 +++++++++---------- .../discodeit/service/UserService.java | 12 +++++++--- .../service/basic/BasicDomainService.java | 2 +- .../service/basic/BasicUserService.java | 19 ++++++---------- 9 files changed, 62 insertions(+), 51 deletions(-) delete mode 100644 src/main/java/com/sprint/mission/discodeit/dto/user/request/UserFindRequest.java diff --git a/src/main/java/com/sprint/mission/discodeit/controller/UserController.java b/src/main/java/com/sprint/mission/discodeit/controller/UserController.java index 5a1a084e0..bfc29cd6d 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/UserController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/UserController.java @@ -1,14 +1,14 @@ package com.sprint.mission.discodeit.controller; +import com.sprint.mission.discodeit.common.util.TimeConverter; import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.dto.user.request.UserCreateRequest; import com.sprint.mission.discodeit.dto.user.request.UserUpdateRequest; import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusDto; +import com.sprint.mission.discodeit.dto.userstatus.UserStatusServiceDTO.UserStatusResponse; import com.sprint.mission.discodeit.dto.userstatus.request.UserStatusUpdateRequest; -import com.sprint.mission.discodeit.mapper.UserMapper; -import com.sprint.mission.discodeit.mapper.UserStatusMapper; import com.sprint.mission.discodeit.service.UserService; import com.sprint.mission.discodeit.service.UserStatusService; import jakarta.annotation.Nullable; @@ -30,8 +30,6 @@ public class UserController { private final UserService userService; private final UserStatusService userStatusService; - private final UserMapper userMapper; - private final UserStatusMapper statusMapper; @GetMapping(value = "/{id}") public ResponseEntity find(@PathVariable UUID id) { @@ -46,9 +44,9 @@ public ResponseEntity> findAll() { @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity create( @RequestPart @Valid UserCreateRequest userCreateRequest, - @RequestPart @Nullable MultipartFile profile) { - BinaryContentDto profileImageDto = getBinaryContentCreateDto(profile); - UserDto userDto = userMapper.toDtoFromCreateRequest(userCreateRequest, profileImageDto); + @RequestPart(name = "profile") @Nullable MultipartFile file) { + BinaryContentDto profile = getBinaryContentCreateDto(file); + UserDto userDto = new UserDto(userCreateRequest, profile); return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(userDto)); } @@ -58,7 +56,7 @@ public ResponseEntity update( @RequestPart @Valid UserUpdateRequest userUpdateRequest, @RequestPart @Nullable MultipartFile profile) { BinaryContentDto profileImageDto = getBinaryContentCreateDto(profile); - UserDto userDto = userMapper.toDtoFromUpdateRequest(id, userUpdateRequest, profileImageDto); + UserDto userDto = new UserDto(id, userUpdateRequest, profileImageDto); return ResponseEntity.status(HttpStatus.OK).body(userService.update(userDto)); } @@ -69,16 +67,16 @@ public ResponseEntity delete(@PathVariable UUID id) { } @PatchMapping(value = "/{id}/userStatus") - public ResponseEntity updateUserStatus( + public ResponseEntity updateUserStatus( @PathVariable UUID id, @RequestBody UserStatusUpdateRequest request) { - UserStatusDto statusDto = statusMapper.toEntity(id, request); + UserStatusDto statusDto = new UserStatusDto(null, id, TimeConverter.toInstant(request.datetime())); return ResponseEntity.status(HttpStatus.OK).body(userStatusService.update(statusDto)); } private BinaryContentDto getBinaryContentCreateDto(MultipartFile profile) { return Optional.ofNullable(profile) - .map(UserMapper.profileImageMapper::toDtoFromFile) + .map(BinaryContentDto::from) .orElse(null); } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java index d4f88030d..13501aca7 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/user/UserServiceDTO.java @@ -1,16 +1,29 @@ package com.sprint.mission.discodeit.dto.user; import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; +import com.sprint.mission.discodeit.dto.user.request.UserCreateRequest; +import com.sprint.mission.discodeit.dto.user.request.UserUpdateRequest; +import lombok.Builder; import java.util.UUID; public interface UserServiceDTO { // todo: error log - record UserResponse(UUID id, String username, String email, boolean online) { + @Builder + record UserResponse(UUID id, String username, String email, boolean online, + BinaryContentResponse profile) { } record UserDto(UUID id, String username, String email, String password, BinaryContentDto profile, boolean online) implements UserUpdateDto, UserCreateDto { + public UserDto(UserCreateRequest request, BinaryContentDto profileDto) { + this(null, request.username(), request.email(), request.password(), profileDto, false); + } + + public UserDto(UUID id, UserUpdateRequest request, BinaryContentDto profileDto) { + this(id, request.username(), request.email(), request.password(), profileDto, false); + } } interface UserUpdateDto extends UserUniquenessDto { diff --git a/src/main/java/com/sprint/mission/discodeit/dto/user/request/UserCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/user/request/UserCreateRequest.java index 82acc1f92..60e4d48d4 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/user/request/UserCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/user/request/UserCreateRequest.java @@ -1,9 +1,9 @@ package com.sprint.mission.discodeit.dto.user.request; -import jakarta.annotation.Nonnull; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; -public record UserCreateRequest(@Nonnull String username, - @Nonnull @Email String email, - @Nonnull String password) { +public record UserCreateRequest(@NotNull String username, + @NotNull @Email String email, + @NotNull String password) { } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/user/request/UserFindRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/user/request/UserFindRequest.java deleted file mode 100644 index 29bd05006..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/user/request/UserFindRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.dto.user.request; - -import jakarta.annotation.Nonnull; - -public record UserFindRequest(@Nonnull String username, @Nonnull String password) { -} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/User.java b/src/main/java/com/sprint/mission/discodeit/entity/User.java index ada4b06a8..c09a3b624 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/User.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/User.java @@ -1,5 +1,6 @@ package com.sprint.mission.discodeit.entity; +import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserCreateDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUpdateDto; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; @@ -23,12 +24,12 @@ public class User extends BaseUpdatableEntity { @Column(nullable = false, length = 60) private String password; - @OneToOne + @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "profile_id") private BinaryContent profile; @Setter - @OneToOne(mappedBy = "users") + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) private UserStatus status; public User(String username, String email, String password, @@ -41,6 +42,10 @@ public User(String username, String email, String password, this.status.setUser(this); } + public User(UserCreateDto dto, BinaryContent profile, UserStatus status) { + this(dto.username(), dto.email(), dto.password(), profile, status); + } + public boolean isOnline() { return status.isOnline(); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java index dd7cd76c8..596800142 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java @@ -1,21 +1,15 @@ package com.sprint.mission.discodeit.mapper; -import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserCreateDto; +import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; -import com.sprint.mission.discodeit.dto.user.request.UserCreateRequest; -import com.sprint.mission.discodeit.dto.user.request.UserUpdateRequest; -import com.sprint.mission.discodeit.entity.BinaryContent; import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.entity.UserStatus; import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.mapstruct.factory.Mappers; -import java.util.UUID; - @Mapper(config = GlobalMapperConfig.class) public interface UserMapper extends BaseMapper { BinaryContentMapper profileImageMapper = Mappers.getMapper(BinaryContentMapper.class); @@ -24,9 +18,15 @@ public interface UserMapper extends BaseMapper { @Mapping(source = "online", target = "online") UserDto toDto(User entity); - User toEntity(UserCreateDto createDto, BinaryContent profile, UserStatus status); + @Override + @Mapping(target = "online", source = "online") + @Mapping(target = "profile", source = "user", qualifiedByName = "profileToResponse") + UserResponse toResponse(User user); - UserDto toDtoFromCreateRequest(UserCreateRequest request, BinaryContentDto profile); + @Named("profileToResponse") + default BinaryContentResponse profileToResponse(User user) { + return profileImageMapper.toResponse(user.getProfile()); + } - UserDto toDtoFromUpdateRequest(UUID id, UserUpdateRequest request, BinaryContentDto profile); + User toEntity(UserResponse response); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/UserService.java b/src/main/java/com/sprint/mission/discodeit/service/UserService.java index 8a66657ad..b74dc5b63 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/UserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/UserService.java @@ -3,12 +3,18 @@ import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserCreateDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUpdateDto; -import com.sprint.mission.discodeit.dto.user.request.UserFindRequest; import java.util.List; +import java.util.UUID; -public interface UserService extends DomainService { - UserResponse find(UserFindRequest request); +public interface UserService { + UserResponse create(UserCreateDto dto); + + UserResponse find(UUID id); List findAll(); + + UserResponse update(UserUpdateDto dto); + + void delete(UUID id); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java index ab96bb109..5e0f55656 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDomainService.java @@ -23,7 +23,7 @@ protected void deleteByIdOrThrow(UUID id, JpaRepository repository, API } protected void ensure(R value, Function condition, Function exception) { - if (condition.apply(value)) { + if (!condition.apply(value)) { return; } throw exception.apply(value); diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java index 0fd4e1f98..cd24b845b 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java @@ -7,7 +7,6 @@ import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUniquenessDto; import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserUpdateDto; -import com.sprint.mission.discodeit.dto.user.request.UserFindRequest; import com.sprint.mission.discodeit.entity.BinaryContent; import com.sprint.mission.discodeit.entity.User; import com.sprint.mission.discodeit.entity.UserStatus; @@ -16,31 +15,27 @@ import com.sprint.mission.discodeit.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; import java.util.UUID; -//@Transactional, not yet +@Transactional @Service @RequiredArgsConstructor public class BasicUserService extends BasicDomainService implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; - // deprecated ? @Override - public UserResponse find(UserFindRequest request) { - return null; - } - - // deprecated ? - @Override - public UserResponse find(UUID userId) { - return null; + @Transactional(readOnly = true) + public UserResponse find(UUID id) { + return userMapper.toResponse(findById(id)); } @Override + @Transactional(readOnly = true) public List findAll() { return userRepository.findAll() .stream() @@ -53,7 +48,7 @@ public UserResponse create(UserCreateDto dto) { validateUserUniqueness(dto); UserStatus status = new UserStatus(); BinaryContent profile = registerProfile(dto.profile()); - User user = userMapper.toEntity(dto, profile, status); + User user = new User(dto, profile, status); userRepository.save(user); return userMapper.toResponse(user); } From 7a3499dc16d41aa61ed5cc51136128844553a0b8 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 00:13:18 +0900 Subject: [PATCH 21/28] build(gradle, docker-compose): add dependencies, change db port - build.gradle: add dependencies analysis, openfeign.QueryDSL - docker-compose.yml: db port 5432 -> 5433 --- build.gradle | 34 ++++++++++++++++++++---- docker-compose.yml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index b0ab8088e..06ca758a8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.10' id "io.spring.dependency-management" version '1.1.6' + id "com.autonomousapps.dependency-analysis" version "3.6.1" } group = 'com.sprint.mission' @@ -19,6 +20,7 @@ dependencies { compileOnly("org.projectlombok:lombok:1.18.42") annotationProcessor("org.projectlombok:lombok:1.18.42") implementation('org.mapstruct:mapstruct:1.6.3') + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' annotationProcessor('org.mapstruct:mapstruct-processor:1.6.3') implementation('org.springframework.boot:spring-boot-starter-web') @@ -27,16 +29,38 @@ dependencies { implementation('org.springdoc:springdoc-openapi-starter-webmvc-ui') implementation('org.springframework.boot:spring-boot-starter-data-jpa') + implementation 'io.github.openfeign.querydsl:querydsl-jpa:7.1' + annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:7.1:jpa' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + developmentOnly('org.springframework.boot:spring-boot-devtools') runtimeOnly('org.postgresql:postgresql:42.7.7') testCompileOnly("org.projectlombok:lombok:1.18.42") testAnnotationProcessor("org.projectlombok:lombok:1.18.42") testImplementation('org.springframework.boot:spring-boot-starter-test') - developmentOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'net.datafaker:datafaker:2.5.4' + testImplementation('net.datafaker:datafaker:2.5.4') +} + +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +tasks.withType(JavaCompile).configureEach { + options.getGeneratedSourceOutputDirectory().set(querydslDir) +} + +clean { + delete file(querydslDir) +} + +jar { + enabled = false } -test { - useJUnitPlatform() -} \ No newline at end of file +//test { +// useJUnitPlatform() +//} diff --git a/docker-compose.yml b/docker-compose.yml index 6b0aff22a..9ff1410be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,4 @@ services: POSTGRES_PASSWORD: discodeit1234 POSTGRES_DB: discodeit ports: - - "5432:5432" + - "5433:5432" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ce2191157..912432c2f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Dec 02 14:48:55 KST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 2d32cbbbaad9ca7e87379ebd132825f7d6c1a1e0 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 00:43:18 +0900 Subject: [PATCH 22/28] feat(Channel): try to resolve N+1 problem To retrieve the 'lastMessageAt' of a 'channel', the associated 'Message' must be retrieved. This process causes the 'N+1 problem'. A simple solution would be to use a 'fetch join'. However, 'QueryDSL' was chosen to further improve performance. Querying a single 'Channel' alone used 2 'Query's. If this method were applied to query multiple 'Channels', it would generate N+N 'Query's, so a different approach was used to resolve this. Consequently, it was concluded that using 'Native SQL' is the best option. --- .../discodeit/config/QueryDslConfig.java | 20 +++++ .../mission/discodeit/config/WebConfig.java | 2 + .../controller/ChannelController.java | 9 +-- .../dto/channel/ChannelServiceDTO.java | 11 ++- .../mission/discodeit/entity/Channel.java | 36 +++------ .../entity/base/BaseUpdatableEntity.java | 3 +- .../discodeit/mapper/ChannelMapper.java | 17 ++-- .../repository/ChannelRepository.java | 3 +- .../querydsl/ChannelQDSLRepository.java | 15 ++++ .../querydsl/ChannelQDSLRepositoryImpl.java | 80 +++++++++++++++++++ .../service/basic/BasicChannelService.java | 57 ++++++------- src/main/resources/application-dev.yml | 5 +- {db => src/main/resources/db}/schema.sql | 3 +- 13 files changed, 178 insertions(+), 83 deletions(-) create mode 100644 src/main/java/com/sprint/mission/discodeit/config/QueryDslConfig.java create mode 100644 src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepository.java create mode 100644 src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepositoryImpl.java rename {db => src/main/resources/db}/schema.sql (97%) diff --git a/src/main/java/com/sprint/mission/discodeit/config/QueryDslConfig.java b/src/main/java/com/sprint/mission/discodeit/config/QueryDslConfig.java new file mode 100644 index 000000000..0b97f1e7f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/config/QueryDslConfig.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class QueryDslConfig { + @PersistenceContext + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/config/WebConfig.java b/src/main/java/com/sprint/mission/discodeit/config/WebConfig.java index 2d9bf855f..ac467ec98 100644 --- a/src/main/java/com/sprint/mission/discodeit/config/WebConfig.java +++ b/src/main/java/com/sprint/mission/discodeit/config/WebConfig.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; @@ -13,6 +14,7 @@ import java.util.List; @Configuration +@EnableJpaAuditing @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { private final ObjectMapper objectMapper; diff --git a/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java b/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java index 6f99a65ff..47c2ac933 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java @@ -5,9 +5,7 @@ import com.sprint.mission.discodeit.dto.channel.request.PrivateChannelCreateRequest; import com.sprint.mission.discodeit.dto.channel.request.PublicChannelCreateRequest; import com.sprint.mission.discodeit.dto.channel.request.PublicChannelUpdateRequest; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.UserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -22,7 +20,6 @@ @RequiredArgsConstructor public class ChannelController { private final ChannelService channelService; - private final UserService userService; @PostMapping(value = "/public") public ResponseEntity create(@RequestBody @Valid PublicChannelCreateRequest request) { @@ -37,12 +34,8 @@ public ResponseEntity create(@RequestBody @Valid PublicChannelC @PostMapping(value = "/private") public ResponseEntity create(@RequestBody @Valid PrivateChannelCreateRequest request) { - List participants = request.participantIds() - .stream() - .map(userService::find) - .toList(); ChannelDto dto = ChannelDto.builder() - .participants(participants) + .participantIds(request.participantIds()) .type(request.type()) .build(); return ResponseEntity.status(HttpStatus.CREATED) diff --git a/src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java index 6f8648d2d..f792ded3d 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/channel/ChannelServiceDTO.java @@ -5,6 +5,7 @@ import lombok.Builder; import java.time.Instant; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -12,12 +13,12 @@ public interface ChannelServiceDTO { // todo: error log @Builder record ChannelResponse(UUID id, ChannelType type, String name, String description, - List participants, Instant lastMessageAt) { + List participants, LocalDateTime lastMessageAt) { } @Builder record ChannelDto(UUID id, String name, String description, ChannelType type, - List participants, Instant lastMessageAt) + List participantIds, Instant lastMessageAt) implements PublicChannelCreateDto, PrivateChannelCreateDto, PublicChannelUpdateDto { } @@ -27,12 +28,16 @@ interface PublicChannelCreateDto { String description(); ChannelType type(); + + Instant lastMessageAt(); } interface PrivateChannelCreateDto { - List participants(); + List participantIds(); ChannelType type(); + + Instant lastMessageAt(); } interface PublicChannelUpdateDto { diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java index fdab08f06..3ccb41080 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java @@ -1,17 +1,14 @@ package com.sprint.mission.discodeit.entity; import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelDto; -import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PrivateChannelCreateDto; -import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PublicChannelCreateDto; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; +import java.time.Instant; @Getter @NoArgsConstructor @@ -28,17 +25,15 @@ public class Channel extends BaseUpdatableEntity { @Column(nullable = false) private String description; - private List participants = new ArrayList<>(); + @Setter + @Transient + private Instant lastMessageAt; - public Channel(PublicChannelCreateDto dto) { - this.name = dto.name(); - this.description = dto.description(); - this.type = dto.type(); - } - - public Channel(PrivateChannelCreateDto dto) { - participants = dto.participants(); - this.type = dto.type(); + @Builder + public Channel(ChannelType type, String name, String description) { + this.type = type; + this.name = name; + this.description = description; } @Override @@ -46,13 +41,4 @@ public void update(ChannelDto dto) { updateIfChanged(name, dto.name(), val -> name = val); updateIfChanged(description, dto.description(), val -> description = val); } - - public boolean matchChannelType(ChannelType type) { - return this.type == type; - } - - public boolean isVisibleTo(UUID userId) { - return type == ChannelType.PUBLIC || - participants.stream().anyMatch(u -> u.id().equals(userId)); - } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java index 8b6938c9f..acc572b98 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/base/BaseUpdatableEntity.java @@ -8,6 +8,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.Instant; +import java.util.Objects; import java.util.function.Consumer; @Getter @@ -19,7 +20,7 @@ public abstract class BaseUpdatableEntity extends BaseEntity { private Instant updatedAt; protected void updateIfChanged(T before, T after, Consumer action) { - if (after == null || before.equals(after)) { + if (after == null || Objects.equals(before, after)) { return; } action.accept(after); diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java index 88406172f..fbe078e02 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java @@ -2,23 +2,18 @@ import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelDto; import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.ChannelResponse; -import com.sprint.mission.discodeit.dto.channel.request.PrivateChannelCreateRequest; -import com.sprint.mission.discodeit.dto.channel.request.PublicChannelCreateRequest; -import com.sprint.mission.discodeit.dto.channel.request.PublicChannelUpdateRequest; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PrivateChannelCreateDto; +import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PublicChannelCreateDto; import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; - -import java.util.UUID; @Mapper(config = GlobalMapperConfig.class) public interface ChannelMapper extends BaseMapper { - UserMapper userMapper = Mappers.getMapper(UserMapper.class); - - ChannelDto toDtoFromRequest(PublicChannelCreateRequest request); + @Override + ChannelResponse toResponse(Channel entity); - ChannelDto toDtoFromRequest(PrivateChannelCreateRequest request); + Channel fromDto(PublicChannelCreateDto dto); - ChannelDto toDtoFromRequest(UUID id, PublicChannelUpdateRequest request); + Channel fromDto(PrivateChannelCreateDto dto); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java index 5249155b1..f2c23e10f 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java @@ -1,9 +1,10 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.repository.querydsl.ChannelQDSLRepository; import org.springframework.data.jpa.repository.JpaRepository; import java.util.UUID; -public interface ChannelRepository extends JpaRepository { +public interface ChannelRepository extends JpaRepository, ChannelQDSLRepository { } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepository.java new file mode 100644 index 000000000..6de7f3d18 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepository.java @@ -0,0 +1,15 @@ +package com.sprint.mission.discodeit.repository.querydsl; + +import com.sprint.mission.discodeit.entity.Channel; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ChannelQDSLRepository { + Optional findByIdWithLastMsgAt(UUID id); + + List findAllWithLastMsgAt(); + + List findVisibleToWithLastMsgAt(UUID userId); +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepositoryImpl.java b/src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepositoryImpl.java new file mode 100644 index 000000000..4c744a6f4 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/querydsl/ChannelQDSLRepositoryImpl.java @@ -0,0 +1,80 @@ +package com.sprint.mission.discodeit.repository.querydsl; + +import com.querydsl.core.Tuple; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLSubQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.sprint.mission.discodeit.entity.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +@RequiredArgsConstructor +@Repository +public class ChannelQDSLRepositoryImpl implements ChannelQDSLRepository { + private final JPAQueryFactory queryFactory; + private final QChannel qChannel = QChannel.channel; + private final QMessage qMessage = QMessage.message; + private final QReadStatus qReadStatus = QReadStatus.readStatus; + + @Override + public List findVisibleToWithLastMsgAt(UUID userId) { + return queryFactory.select(qChannel, getLastMsgAtSubQuery()) + .from(qChannel) + .where(qChannel.type.eq(ChannelType.PUBLIC) + .or(qChannel.id.in(getJoinedPrivateChannels(userId)))) + .fetch() + .stream() + .map(this::injectLastMsgAt) + .toList(); + } + + @Override + public Optional findByIdWithLastMsgAt(UUID id) { + Channel channel = queryFactory.selectFrom(qChannel) + .where(qChannel.id.eq(id)) + .fetchOne(); + Optional result = Optional.ofNullable(channel); + Instant lastMessageAt = queryFactory.select(qMessage.createdAt.max()) + .from(qMessage) + .where(qMessage.channel.id.eq(id)) + .fetchOne(); + result.ifPresent(c -> c.setLastMessageAt(lastMessageAt)); + return result; + } + + @Override + public List findAllWithLastMsgAt() { + return queryFactory + .select(qChannel, getLastMsgAtSubQuery()) + .from(qChannel) + .fetch() + .stream() + .map(this::injectLastMsgAt) + .toList(); + } + + private Channel injectLastMsgAt(Tuple tuple) { + Channel channel = tuple.get(qChannel); + Instant lastMessageAt = tuple.get(getLastMsgAtSubQuery()); + Objects.requireNonNull(channel).setLastMessageAt(lastMessageAt); + return channel; + } + + private JPQLSubQuery getLastMsgAtSubQuery() { + return JPAExpressions.select(qMessage.createdAt.max()) + .from(qMessage) + .where(qMessage.channel.id.eq(qChannel.id)); + } + + private JPQLSubQuery getJoinedPrivateChannels(UUID userId) { + return JPAExpressions.select(qReadStatus.channel.id) + .from(qReadStatus) + .where(qReadStatus.user.id.eq(userId)); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java index 850e0cdd8..d7bc451e0 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java @@ -8,94 +8,87 @@ import com.sprint.mission.discodeit.dto.channel.ChannelServiceDTO.PublicChannelCreateDto; import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.entity.ChannelType; -import com.sprint.mission.discodeit.entity.Message; import com.sprint.mission.discodeit.entity.ReadStatus; import com.sprint.mission.discodeit.mapper.ChannelMapper; import com.sprint.mission.discodeit.repository.ChannelRepository; -import com.sprint.mission.discodeit.repository.MessageRepository; import com.sprint.mission.discodeit.repository.ReadStatusRepository; +import com.sprint.mission.discodeit.repository.UserRepository; import com.sprint.mission.discodeit.service.ChannelService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.List; import java.util.UUID; -@Service @RequiredArgsConstructor +@Service +@Transactional public class BasicChannelService extends BasicDomainService implements ChannelService { + private final UserRepository userRepository; private final ChannelRepository channelRepository; private final ReadStatusRepository readStatusRepository; - private final MessageRepository messageRepository; private final ChannelMapper channelMapper; @Override public ChannelResponse createPublic(PublicChannelCreateDto dto) { - Channel channel = new Channel(dto); + Channel channel = channelMapper.fromDto(dto); channelRepository.save(channel); return channelMapper.toResponse(channel); } + // todo: [warn] user id not found @Override public ChannelResponse createPrivate(PrivateChannelCreateDto dto) { - Channel channel = new Channel(dto); + Channel channel = channelMapper.fromDto(dto); channelRepository.save(channel); - dto.participants() + + // todo: possible to be tuned ? + List readStatuses = userRepository.findAllById(dto.participantIds()) .stream() - .map(ChannelMapper.userMapper::toEntity) - .map(user -> new ReadStatus(user, channel, Instant.MIN)) - .forEach(readStatusRepository::save); + .map(user -> new ReadStatus(user, channel, Instant.now())) + .toList(); + + readStatusRepository.saveAll(readStatuses); return channelMapper.toResponse(channel); } @Override + @Transactional(readOnly = true) public ChannelResponse find(UUID id) { - return channelMapper.toResponse(findById(id)); + Channel channel = findById(id); + return channelMapper.toResponse(channel); } @Override + @Transactional(readOnly = true) public List findAllByUserId(UUID userId) { - return channelRepository.findAll() + return channelRepository.findVisibleToWithLastMsgAt(userId) .stream() - .filter(channel -> channel.isVisibleTo(userId)) .map(channelMapper::toResponse) .toList(); } @Override public ChannelResponse update(ChannelDto dto) { + if (dto.type() == ChannelType.PRIVATE) { + throw new APIException(ErrorCode.PRIVATE_CHANNEL_CANT_BE_UPDATED, dto.id()); + } Channel channel = findById(dto.id()); -// MessageResponse lastMsgResp = getLastMessageResponse(channel.getId()); - ensure(ChannelType.PRIVATE, - channel::matchChannelType, - type -> new APIException(ErrorCode.PRIVATE_CHANNEL_NOT_UPDATE, dto.id())); channel.update(dto); - channelRepository.save(channel); return channelMapper.toResponse(channel); } @Override public void delete(UUID id) { - ensure(id, channelRepository::existsById, val -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, val)); - List msgToDelete = messageRepository.findAllByChannelId(id); - messageRepository.deleteAll(msgToDelete); - List readStatuses = readStatusRepository.findAllByChannelId(id); - readStatusRepository.deleteAll(readStatuses); - channelRepository.deleteById(id); + deleteByIdOrThrow(id, channelRepository, new APIException(ErrorCode.CHANNELID_NOT_FOUND, id)); } @Override protected Channel findById(UUID id) { - return getOrThrow(id, channelRepository::findById, + return getOrThrow(id, channelRepository::findByIdWithLastMsgAt, () -> new APIException(ErrorCode.CHANNELID_NOT_FOUND, id)); } - // deprecated ? -// private MessageDto getLastMessageResponse(UUID channelId) { -// return messageRepository.filter(message -> message.isInChannel(channelId)) -// .max(Message::compareTo) -// .orElseThrow(() -> new NoSuchElementException("this channel have no message")) -// .toResponse(); -// } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index df9f1355d..4f1f95618 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -15,7 +15,7 @@ spring: schema-locations: classpath:db/schema.sql datasource: - url: jdbc:postgresql://localhost:5432/discodeit + url: jdbc:postgresql://localhost:5433/discodeit username: discodeit_user password: discodeit1234 @@ -28,6 +28,9 @@ spring: properties: hibernate: format_sql: true + jdbc.batch_size: 100 + order_inserts: true + order_updates: true logging: level: diff --git a/db/schema.sql b/src/main/resources/db/schema.sql similarity index 97% rename from db/schema.sql rename to src/main/resources/db/schema.sql index e9330a278..a04a58035 100644 --- a/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -39,6 +39,7 @@ create table messages channel_id uuid not null, author_id uuid not null ); +create index idx_messages on messages (channel_id, created_at); create table binary_contents ( @@ -93,4 +94,4 @@ alter table read_statuses add constraint uk_read_statuses_user_channel_id unique (user_id, channel_id); alter table message_attachments add constraint fk_message_attachments_message_id foreign key (message_id) references messages (id) on delete cascade, - add constraint fk_message_attachments_attachment_id foreign key (attachment_id) references binary_contents (id) on delete cascade; \ No newline at end of file + add constraint fk_message_attachments_attachment_id foreign key (attachment_id) references binary_contents (id) on delete cascade; From 2f683e3049fed87dfb208798dead75afa6b3efb1 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 00:51:12 +0900 Subject: [PATCH 23/28] feat(common): update advice, code, util - APIExceptionAdvice: rename handleIdNotFound -> handleAPIException - APIExceptionAdvice: add handleIncorrectInput - ErrorCode: add PRIVATE_CHANNEL_CANT_BE_UPDATED - TimeConverter: change LocalDateTime(deprecated) to LocalDate --- .../common/exception/advice/APIExceptionAdvice.java | 13 ++++++++++++- .../discodeit/common/exception/code/ErrorCode.java | 2 +- .../discodeit/common/util/TimeConverter.java | 10 +++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/sprint/mission/discodeit/common/exception/advice/APIExceptionAdvice.java b/src/main/java/com/sprint/mission/discodeit/common/exception/advice/APIExceptionAdvice.java index 08ecddb96..abddc170c 100644 --- a/src/main/java/com/sprint/mission/discodeit/common/exception/advice/APIExceptionAdvice.java +++ b/src/main/java/com/sprint/mission/discodeit/common/exception/advice/APIExceptionAdvice.java @@ -5,6 +5,7 @@ import com.sprint.mission.discodeit.common.exception.model.APIExceptionModel; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -12,7 +13,7 @@ public class APIExceptionAdvice { @ExceptionHandler(APIException.class) - private ProblemDetail handleIdNotFound(APIException exception, HttpServletRequest request) { + private ProblemDetail handleAPIException(APIException exception, HttpServletRequest request) { ErrorCode errorCode = exception.getErrorCode(); return APIExceptionModel.builder(errorCode.getStatus()) .detail(exception.getDetail()) @@ -21,4 +22,14 @@ private ProblemDetail handleIdNotFound(APIException exception, HttpServletReques .uri(request.getRequestURI()) .build(); } + + // todo + @ExceptionHandler(MethodArgumentNotValidException.class) + private ProblemDetail handleIncorrectInput(MethodArgumentNotValidException exception, HttpServletRequest request) { + return APIExceptionModel.builder(exception.getStatusCode().value()) + .detail(exception.getLocalizedMessage()) + .method(request.getMethod()) + .uri(request.getRequestURI()) + .build(); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java b/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java index 914f7a05a..ff7f5d6eb 100644 --- a/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java +++ b/src/main/java/com/sprint/mission/discodeit/common/exception/code/ErrorCode.java @@ -23,7 +23,7 @@ public enum ErrorCode { // channel CHANNELID_NOT_FOUND(404, "C001", "Channel id not found"), NO_MESSAGE_IN_CHANNEL(404, "C002", "channel have no messages"), - PRIVATE_CHANNEL_NOT_UPDATE(400, "C003", "Private Channel can`t be updated"), + PRIVATE_CHANNEL_CANT_BE_UPDATED(400, "C003", "Private Channel can`t be updated"), // message MESSAGEID_NOT_FOUND(404, "M001", "Message id not found"), diff --git a/src/main/java/com/sprint/mission/discodeit/common/util/TimeConverter.java b/src/main/java/com/sprint/mission/discodeit/common/util/TimeConverter.java index b0f33fdd9..ac5caaa76 100644 --- a/src/main/java/com/sprint/mission/discodeit/common/util/TimeConverter.java +++ b/src/main/java/com/sprint/mission/discodeit/common/util/TimeConverter.java @@ -1,15 +1,15 @@ package com.sprint.mission.discodeit.common.util; import java.time.Instant; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.time.ZoneId; public interface TimeConverter { - static LocalDateTime toDateTime(Instant time) { - return LocalDateTime.ofInstant(time, ZoneId.systemDefault()); + static LocalDate toDateTime(Instant time) { + return LocalDate.ofInstant(time, ZoneId.systemDefault()); } - static Instant toInstant(LocalDateTime datetime) { - return datetime.atZone(ZoneId.systemDefault()).toInstant(); + static Instant toInstant(LocalDate datetime) { + return datetime.atStartOfDay(ZoneId.systemDefault()).toInstant(); } } From 4a0657d86e8944bf776b55aba4d2b656bec84d8e Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 00:58:15 +0900 Subject: [PATCH 24/28] feat(entity, dto): update entity, dto - update entity: Message, ReadStatus, UserStatus - update dto: BinaryContentServiceDTO, MessageServiceDTO, UserStatusServiceDTO --- .../binarycontent/BinaryContentServiceDTO.java | 16 ++++++++++++++++ .../discodeit/dto/message/MessageServiceDTO.java | 6 +++++- .../dto/userstatus/UserStatusServiceDTO.java | 4 +--- .../sprint/mission/discodeit/entity/Message.java | 2 +- .../mission/discodeit/entity/ReadStatus.java | 2 ++ .../mission/discodeit/entity/UserStatus.java | 8 ++++++-- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java index 803cedb38..d5b91cde9 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/binarycontent/BinaryContentServiceDTO.java @@ -1,7 +1,11 @@ package com.sprint.mission.discodeit.dto.binarycontent; +import com.sprint.mission.discodeit.common.exception.code.ErrorCode; +import com.sprint.mission.discodeit.common.exception.custom.APIException; import lombok.Builder; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.UUID; public interface BinaryContentServiceDTO { @@ -10,5 +14,17 @@ record BinaryContentResponse(UUID id, String fileName, Long size, String content @Builder record BinaryContentDto(UUID id, String fileName, Long size, String contentType, byte[] bytes) { + public static BinaryContentDto from(MultipartFile file) { + try { + return BinaryContentDto.builder() + .fileName(file.getOriginalFilename()) + .size(file.getSize()) + .contentType(file.getContentType()) + .bytes(file.getBytes()) + .build(); + } catch (IOException e) { + throw new APIException(ErrorCode.FILE_CANT_READ, e.getMessage()); + } + } } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java index b00a0eaed..98228e448 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/message/MessageServiceDTO.java @@ -22,22 +22,26 @@ record MessageResponse(UUID id, UserResponse author, ChannelResponse channel, St @Builder record MessageDto(UUID id, UUID authorId, UUID channelId, String content, List attachments) - implements MessageCreateDto, MessageUpdateDto { + implements MessageCreateDto, MessageUpdateDto { } interface MessageCreateDto extends AuthorAndChannelId { String content(); + List attachments(); } interface MessageUpdateDto extends AuthorAndChannelId { UUID id(); + String content(); + List attachments(); } interface AuthorAndChannelId { UUID authorId(); + UUID channelId(); } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java index 1e1395190..dba271b25 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/UserStatusServiceDTO.java @@ -1,13 +1,11 @@ package com.sprint.mission.discodeit.dto.userstatus; -import com.sprint.mission.discodeit.dto.user.UserServiceDTO.UserResponse; - import java.time.Instant; import java.time.LocalDateTime; import java.util.UUID; public interface UserStatusServiceDTO { - record UserStatusResponse(UUID id, UserResponse user, LocalDateTime lastActiveAt) { + record UserStatusResponse(UUID id, UUID userId, LocalDateTime lastActiveAt) { } record UserStatusDto(UUID id, UUID userId, Instant lastActiveAt) { diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Message.java b/src/main/java/com/sprint/mission/discodeit/entity/Message.java index 8a17435e0..a53c41b54 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Message.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Message.java @@ -26,7 +26,7 @@ public class Message extends BaseUpdatableEntity { @JoinColumn(name = "author_id") private User author; - @OneToMany + @OneToMany(mappedBy = "messages") private List attachments = new ArrayList<>(); @Builder diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java index 05d0bea3f..7e37e604f 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java @@ -3,10 +3,12 @@ import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusUpdateDto; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import jakarta.persistence.*; +import lombok.Getter; import lombok.NoArgsConstructor; import java.time.Instant; +@Getter @NoArgsConstructor @Entity @Table(name = "read_statuses") diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java index 7f3bd5d62..1e1e910e8 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java @@ -6,14 +6,17 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.Duration; import java.time.Instant; -@NoArgsConstructor @Getter +@NoArgsConstructor @Entity @Table(name = "user_statuses") +@EntityListeners(AuditingEntityListener.class) public class UserStatus extends BaseUpdatableEntity { private static final Long ACTIVE_THRESHOLD = 300L; @@ -22,7 +25,8 @@ public class UserStatus extends BaseUpdatableEntity { @JoinColumn(name = "user_id") private User user; - @Column + @CreatedDate + @Column(nullable = false) private Instant lastActiveAt; public UserStatus(User user, Instant lastActiveAt) { From 7d833bdd950dc54dac64f5a5db914bc5c5d95802 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 01:00:16 +0900 Subject: [PATCH 25/28] feat(mapper): update and init - TimeMapper(util mapper): init - update: GlobalMapperConfig, BinaryContentMapper, ReadStatusMapper, UserMapper, UserStatusMapper --- .../discodeit/mapper/BinaryContentMapper.java | 18 ++++++++++++++++- .../discodeit/mapper/ReadStatusMapper.java | 4 ++++ .../mission/discodeit/mapper/UserMapper.java | 4 ++-- .../discodeit/mapper/UserStatusMapper.java | 10 ++++++++++ .../discodeit/mapper/common/TimeMapper.java | 20 +++++++++++++++++++ .../mapper/config/GlobalMapperConfig.java | 5 ++++- 6 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/sprint/mission/discodeit/mapper/common/TimeMapper.java diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java index 38cf3bcb1..0077f6a56 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/BinaryContentMapper.java @@ -1,5 +1,7 @@ package com.sprint.mission.discodeit.mapper; +import com.sprint.mission.discodeit.common.exception.code.ErrorCode; +import com.sprint.mission.discodeit.common.exception.custom.APIException; import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentDto; import com.sprint.mission.discodeit.dto.binarycontent.BinaryContentServiceDTO.BinaryContentResponse; import com.sprint.mission.discodeit.dto.binarycontent.request.BinaryContentCreateRequest; @@ -8,10 +10,24 @@ import org.mapstruct.Mapper; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; + @Mapper(config = GlobalMapperConfig.class) public interface BinaryContentMapper extends BaseMapper { // todo: add 'read file error' - BinaryContentDto toDtoFromFile(MultipartFile file); + default BinaryContentDto toDtoFromFile(MultipartFile file) { + try { + byte[] bytes = file.getBytes(); + return BinaryContentDto.builder() + .fileName(file.getOriginalFilename()) + .contentType(file.getContentType()) + .size(file.getSize()) + .bytes(bytes) + .build(); + } catch (IOException e) { + throw new APIException(ErrorCode.FILE_CANT_READ, file.getName()); + } + } BinaryContent toEntityFromRequest(BinaryContentCreateRequest request); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java index 47bc40ced..4b1369eed 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java @@ -5,7 +5,11 @@ import com.sprint.mission.discodeit.entity.ReadStatus; import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; @Mapper(config = GlobalMapperConfig.class) public interface ReadStatusMapper extends BaseMapper { + @Override + @Mapping(target = "lastReadAt") + ReadStatusResponse toResponse(ReadStatus entity); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java index 596800142..d74a0831e 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java @@ -23,10 +23,10 @@ public interface UserMapper extends BaseMapper { @Mapping(target = "profile", source = "user", qualifiedByName = "profileToResponse") UserResponse toResponse(User user); + User toEntity(UserResponse response); + @Named("profileToResponse") default BinaryContentResponse profileToResponse(User user) { return profileImageMapper.toResponse(user.getProfile()); } - - User toEntity(UserResponse response); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java index 03ea9cfee..964e765fe 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java @@ -6,10 +6,20 @@ import com.sprint.mission.discodeit.entity.UserStatus; import com.sprint.mission.discodeit.mapper.config.GlobalMapperConfig; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; import java.util.UUID; @Mapper(config = GlobalMapperConfig.class) public interface UserStatusMapper extends BaseMapper { + UserMapper userMapper = Mappers.getMapper(UserMapper.class); + + @Mapping(target = "lastActiveAt", source = "request.datetime") UserStatusDto toEntity(UUID userId, UserStatusUpdateRequest request); + + @Override + @Mapping(target = "userId", source = "user.id") + @Mapping(target = "lastActiveAt") + UserStatusResponse toResponse(UserStatus entity); } diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/common/TimeMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/common/TimeMapper.java new file mode 100644 index 000000000..bc9731135 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/mapper/common/TimeMapper.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.mapper.common; + +import com.sprint.mission.discodeit.common.util.TimeConverter; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +import java.time.Instant; +import java.time.LocalDate; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface TimeMapper { + + default Instant toInstant(LocalDate time) { + return TimeConverter.toInstant(time); + } + + default LocalDate toDateTime(Instant time) { + return TimeConverter.toDateTime(time); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java b/src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java index 73a150b73..8bce2967f 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/config/GlobalMapperConfig.java @@ -1,9 +1,12 @@ package com.sprint.mission.discodeit.mapper.config; +import com.sprint.mission.discodeit.mapper.common.TimeMapper; import org.mapstruct.MapperConfig; import org.mapstruct.MappingConstants; import org.mapstruct.ReportingPolicy; -@MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) +@MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, + unmappedTargetPolicy = ReportingPolicy.IGNORE, + uses = TimeMapper.class) public interface GlobalMapperConfig { } From 6679970324d1bdac41d0edc44d70cee4bcaa537e Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 01:01:10 +0900 Subject: [PATCH 26/28] refactor(service): rename BasicUserStatusService --- .../discodeit/service/basic/BasicUserStatusService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java index 216a06942..e375eab71 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java @@ -21,15 +21,15 @@ public class BasicUserStatusService extends BasicDomainService imple @Override public UserStatusResponse update(UserStatusDto dto) { - UserStatus status = findById(dto.id()); + UserStatus status = findById(dto.userId()); status.update(dto); userStatusRepository.save(status); return userStatusMapper.toResponse(status); } @Override - protected UserStatus findById(UUID id) { - return getOrThrow(id, userStatusRepository::findById, - () -> new APIException(ErrorCode.USERSTATUSID_NOT_FOUND, id)); + protected UserStatus findById(UUID userId) { + return getOrThrow(userId, userStatusRepository::findByUserId, + () -> new APIException(ErrorCode.USERSTATUSID_NOT_FOUND, userId)); } } From ac5f713efdc0eef06d04281168ab7e7da5265b3c Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 01:15:49 +0900 Subject: [PATCH 27/28] feat(entity): add JoinTable --- .../java/com/sprint/mission/discodeit/entity/Message.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Message.java b/src/main/java/com/sprint/mission/discodeit/entity/Message.java index a53c41b54..ebaac257f 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Message.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Message.java @@ -26,7 +26,12 @@ public class Message extends BaseUpdatableEntity { @JoinColumn(name = "author_id") private User author; - @OneToMany(mappedBy = "messages") + @OneToMany + @JoinTable( + name = "message_attachments", + joinColumns = @JoinColumn(name = "message_id"), + inverseJoinColumns = @JoinColumn(name = "attachment_id") + ) private List attachments = new ArrayList<>(); @Builder From b42c961b57ae7d00d083c58b91b66b8b4342abb4 Mon Sep 17 00:00:00 2001 From: 8c8c8c8c8c8 <8c8c8c8c8c8@gmail.com> Date: Mon, 16 Mar 2026 01:17:30 +0900 Subject: [PATCH 28/28] refactor(dto, controller): change type, rename - dto: change LocalDateTime to LocalDate - controller: renaming --- .../mission/discodeit/controller/ReadStatusController.java | 2 +- .../dto/readstatus/request/ReadStatusCreateRequest.java | 4 ++-- .../dto/readstatus/request/ReadStatusUpdateRequest.java | 5 ++--- .../dto/userstatus/request/UserStatusUpdateRequest.java | 5 ++--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java b/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java index 60f05f10e..65e7e7827 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java @@ -1,9 +1,9 @@ package com.sprint.mission.discodeit.controller; import com.sprint.mission.discodeit.common.util.TimeConverter; -import com.sprint.mission.discodeit.dto.readstatus.request.ReadStatusCreateRequest; import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusDto; import com.sprint.mission.discodeit.dto.readstatus.ReadStatusServiceDTO.ReadStatusResponse; +import com.sprint.mission.discodeit.dto.readstatus.request.ReadStatusCreateRequest; import com.sprint.mission.discodeit.dto.readstatus.request.ReadStatusUpdateRequest; import com.sprint.mission.discodeit.service.ReadStatusService; import jakarta.validation.Valid; diff --git a/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java index 16cc8a6a1..13cf13e77 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusCreateRequest.java @@ -3,10 +3,10 @@ import jakarta.validation.constraints.NotNull; import org.springframework.format.annotation.DateTimeFormat; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.UUID; public record ReadStatusCreateRequest(@NotNull UUID userId, @NotNull UUID channelId, - @NotNull @DateTimeFormat LocalDateTime lastReadAt) { + @DateTimeFormat LocalDate lastReadAt) { } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java index fd6a4e9a1..9200fcb65 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/readstatus/request/ReadStatusUpdateRequest.java @@ -1,9 +1,8 @@ package com.sprint.mission.discodeit.dto.readstatus.request; -import jakarta.validation.constraints.NotNull; import org.springframework.format.annotation.DateTimeFormat; -import java.time.LocalDateTime; +import java.time.LocalDate; -public record ReadStatusUpdateRequest(@DateTimeFormat @NotNull LocalDateTime newLastReadAt) { +public record ReadStatusUpdateRequest(@DateTimeFormat LocalDate newLastReadAt) { } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java index 70cf20c16..0f728ae4d 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/userstatus/request/UserStatusUpdateRequest.java @@ -1,10 +1,9 @@ package com.sprint.mission.discodeit.dto.userstatus.request; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; import org.springframework.format.annotation.DateTimeFormat; -import java.time.LocalDateTime; +import java.time.LocalDate; -public record UserStatusUpdateRequest(@NotNull @JsonProperty("newLastActiveAt") @DateTimeFormat LocalDateTime datetime) { +public record UserStatusUpdateRequest(@JsonProperty("newLastActiveAt") @DateTimeFormat LocalDate datetime) { }