Skip to content

Conversation

@dolong2
Copy link
Owner

@dolong2 dolong2 commented Sep 13, 2025

๊ฐœ์š”

  • ๋ณผ๋ฅจ์„ ๋งˆ์šดํŠธํ•˜๋Š” API๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

์ž‘์—…๋‚ด์šฉ

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ์‹œ ๋งˆ์šดํŠธ๋œ ๋ณผ๋ฅจ์„ ๋ฐ˜์˜ํ•ด์„œ ์ƒ์„ฑํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€
  • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์ •๋ณด ์„ธ์ด๋ธŒ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
  • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์š”์ฒญ dto ์ถ”๊ฐ€
  • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์œ ์Šค์ผ€์ด์Šค ์ถ”๊ฐ€
  • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์š”์ฒญ ๊ฐ์ฒด ์ถ”๊ฐ€
  • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
  • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํƒฌํ”Œ๋ฆฟ์™ธ์— ํ•„์š”ํ•œ ํ•ญ๋ชฉ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.

  • ๋กœ์ปฌ์—์„œ ๋นŒ๋“œ๊ฐ€ ์„ฑ๊ณตํ•˜๋‚˜์š”?
  • ์ถ”๊ฐ€(์ˆ˜์ •)ํ•œ ์ฝ”๋“œ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋‚˜์š”?
  • pr ํƒ€์ผ“ ๋ธŒ๋žœ์น˜๊ฐ€ ๋งž๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
  • pr์—์„œ ์ž‘์—…ํ•  ๋‚ด์šฉ๋งŒ ์ž‘์—…๋๋‚˜์š”?
  • ๊ธฐ์กด API์™€ ํ˜ธํ™˜๋˜์ง€ ์•Š๋Š” ์‚ฌํ•ญ์ด ์žˆ๋‚˜์š”?

Summary by CodeRabbit

  • ์‹ ๊ธฐ๋Šฅ

    • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋ณผ๋ฅจ์„ ๋งˆ์šดํŠธํ•˜๋Š” ์‹ ๊ทœ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€(POST /{workspaceId}/volume/{volumeId}/mount). mountPath ๋ฐ readOnly ํŒŒ๋ผ๋ฏธํ„ฐ ์ง€์›.
    • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์ ์šฉ ์‹œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ž๋™ ์žฌ๋ฐฐํฌ๋˜์–ด ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ฆ‰์‹œ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.
    • ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ ์‹œ ๋งˆ์šดํŠธ ์„ค์ •์ด ์ž๋™์œผ๋กœ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
  • ๋ณด์•ˆ

    • ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์—”๋“œํฌ์ธํŠธ์— ์ธ์ฆ ์š”๊ตฌ ์ถ”๊ฐ€.
  • ๋ฌธ์„œ

    • ์ƒˆ ์—”๋“œํฌ์ธํŠธ ๋ฐ ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ ์•ˆ๋‚ด ๋ฐ˜์˜.
  • ํ…Œ์ŠคํŠธ

    • ๋งˆ์šดํŠธ ๋™์ž‘๊ณผ ์˜ค๋ฅ˜ ๊ฒฝ๋กœ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€.

@dolong2 dolong2 self-assigned this Sep 13, 2025
@dolong2 dolong2 added 1๏ธโƒฃ Priority: ์ƒ ์šฐ์„ ์ˆœ์œ„ ์ƒ โœจ Feature ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ labels Sep 13, 2025
@dolong2 dolong2 linked an issue Sep 13, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

Walkthrough

์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ ๋ช…๋ น์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ํ”Œ๋ž˜๊ทธ(-v ...)๋ฅผ ์กฐ๋ฆฝํ•ด ์ถ”๊ฐ€ํ•˜๊ณ , ๋ณผ๋ฅจ์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋งˆ์šดํŠธํ•˜๋Š” ์ƒˆ๋กœ์šด ์œ ์Šค์ผ€์ด์Šคยท์—”๋“œํฌ์ธํŠธ(POST /{workspaceId}/volume/{volumeId}/mount)์™€ saveMount ์˜์†ํ™” ๊ฒฝ๋กœ๋ฅผ ๋„์ž…ํ–ˆ๋‹ค.

Changes

Cohort / File(s) Change Summary
์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ: ๋ณผ๋ฅจ ๋งˆ์šดํŠธ ํ†ตํ•ฉ
src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateContainerServiceImpl.kt, src/test/kotlin/com/dcd/server/core/domain/application/service/CreateContainerServiceImplTest.kt
CreateContainerServiceImpl์— QueryVolumePort ์ฃผ์ž… ์ถ”๊ฐ€. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋งˆ์šดํŠธ ๋ชฉ๋ก์„ ์กฐํšŒํ•ด docker -v ํ”Œ๋ž˜๊ทธ๋ฅผ ์ƒ์„ฑยท์‚ฝ์ž…. ํ…Œ์ŠคํŠธ๋Š” QueryVolumePort ๋ชฉํ‚น์œผ๋กœ ๋นˆ ๋ชฉ๋ก ๋ฐ˜ํ™˜์„ ๊ฒ€์ฆ.
๋ณผ๋ฅจ ๋งˆ์šดํŠธ ์œ ์Šค์ผ€์ด์Šค ๋ฐ ํ…Œ์ŠคํŠธ
src/main/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCase.kt, src/test/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCaseTest.kt
MountVolumeUseCase ์ถ”๊ฐ€: ์›Œํฌ์ŠคํŽ˜์ด์Šค/์†Œ์œ  ๊ฒ€์ฆ ํ›„ VolumeMount ์ƒ์„ฑยท์ €์žฅ ๋ฐ DeployApplicationEvent ๋ฐœํ–‰. ์„ฑ๊ณต ๋ฐ ์˜ค๋ฅ˜ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ํ†ตํ•ฉ/๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€.
ํ”„๋ ˆ์  ํ…Œ์ด์…˜ DTO ๋ฐ ๋งคํ•‘
src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/MountVolumeReqDto.kt, src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt, src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeRequestExtension.kt
MountVolumeReqDto์™€ MountVolumeRequest ์ถ”๊ฐ€ ๋ฐ MountVolumeRequest.toDto() ํ™•์žฅ ํ•จ์ˆ˜ ๋„์ž…(๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜ ํฌํ•จ).
์›น ์–ด๋Œ‘ํ„ฐยท๋ณด์•ˆยทํ…Œ์ŠคํŠธ ๋ณด๊ฐ•
src/main/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapter.kt, src/main/kotlin/com/dcd/server/infrastructure/global/config/SecurityConfig.kt, src/test/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapterTest.kt
POST /{workspaceId}/volume/{volumeId}/mount ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€, SecurityConfig์— ํ•ด๋‹น ๋งค์ฒ˜ ๋“ฑ๋ก, VolumeWebAdapter์— MountVolumeUseCase ์ฃผ์ž… ๋ฐ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€.
์˜์†์„ฑ ํฌํŠธยท์–ด๋Œ‘ํ„ฐ ํ™•์žฅ
src/main/kotlin/com/dcd/server/core/domain/volume/spi/CommandVolumePort.kt, src/main/kotlin/com/dcd/server/persistence/volume/VolumePersistenceAdapter.kt
CommandVolumePort์— saveMount(VolumeMount) ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ๋ฐ VolumePersistenceAdapter์—์„œ ํ•ด๋‹น ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ํ•˜์—ฌ VolumeMount ์˜์†ํ™” ์ง€์›.
๋ฐฐํฌ ํ๋ฆ„ ํ…Œ์ŠคํŠธ ์กฐ์ •
src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt
CreateContainerService ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€ ๋ฐ ํ…Œ์ŠคํŠธ์—์„œ CreateContainerService ํ˜ธ์ถœ ๊ฒ€์ฆ์œผ๋กœ ๊ธฐ์กด docker-create ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ ๋Œ€์ฒด.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as ํด๋ผ์ด์–ธํŠธ
  participant Web as VolumeWebAdapter
  participant UC as MountVolumeUseCase
  participant QV as QueryVolumePort
  participant QA as QueryApplicationPort
  participant CV as CommandVolumePort
  participant EP as ApplicationEventPublisher

  User->>Web: POST /{wsId}/volume/{volumeId}/mount\n?applicationId=...\nbody: {mountPath, readOnly}
  Web->>UC: execute(volumeId, applicationId, reqDto)
  UC->>UC: workspace ํ™•์ธ
  UC->>QV: findById(volumeId)
  UC->>QA: findById(applicationId)
  UC->>CV: saveMount(VolumeMount)
  UC-->>EP: publish(DeployApplicationEvent([applicationId]))
  UC-->>Web: ์™„๋ฃŒ
  Web-->>User: 200 OK
Loading
sequenceDiagram
  autonumber
  participant Deploy as DeployApplicationUseCase
  participant CCS as CreateContainerService
  participant QV as QueryVolumePort
  participant Docker as Docker CLI

  Deploy->>CCS: createContainer(application, externalPort)
  activate CCS
  CCS->>QV: findAllMountByApplication(application)
  QV-->>CCS: [VolumeMount...]
  CCS->>Docker: docker create ... -v volumeName:mountPath[:ro] ... -p ...
  CCS->>Docker: docker network connect ...
  deactivate CCS
Loading

Estimated code review effort

๐ŸŽฏ 3 (Moderate) | โฑ๏ธ ~25 minutes

Possibly related PRs

Poem

๊นก์ด, ๊นก์ดโ€”๋‚œ ํ† ๋ผ ๊ฐœ๋ฐœ์ž! ๐Ÿฐ
๋ณผ๋ฅจ์„ ๋ถ™์—ฌ ์ปจํ…Œ์ด๋„ˆ์— ์ฐฐ์‹น,
-v ๊นƒ๋ฐœ ํŽ„๋Ÿญ, ์ฝ๊ธฐ์ „์šฉ๋„ ์ฒ™์ฒ™.
์ €์žฅํ•˜๊ณ  ํโ€”์žฌ๋ฐฐํฌ ์ฝœ ํ•œ ๋ฒˆ,
์ดˆ์›์—์„  ๋ฐฐํฌ๊ฐ€ ๋˜ ์ถค์ถ˜๋‹ค. ๐ŸŽ‰

Pre-merge checks and finishing touches

โœ… Passed checks (3 passed)
Check name Status Explanation
Description Check โœ… Passed Check skipped - CodeRabbitโ€™s high-level summary is enabled.
Title Check โœ… Passed ์ œ๋ชฉ "๐Ÿ”€ :: [#717] - ๋ณผ๋ฅจ ๋งˆ์šดํŠธ API ์ถ”๊ฐ€"๋Š” ๋ณ€๊ฒฝ์…‹์˜ ํ•ต์‹ฌ์ธ '๋ณผ๋ฅจ ๋งˆ์šดํŠธ API ์ถ”๊ฐ€'๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์š”์•ฝํ•˜๊ณ  ์žˆ์–ด PR ๋ชฉ์ ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค; ๋‹ค๋งŒ ์„ ํ–‰์˜ ์ด๋ชจ์ง€์™€ PR ๋ฒˆํ˜ธ ํ‘œ๊ธฐ๋Š” ๋ถˆํ•„์š”ํ•œ ๋…ธ์ด์ฆˆ๋กœ ๋ณด์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Docstring Coverage โœ… Passed No functions found in the changes. Docstring coverage check skipped.
โœจ Finishing touches
  • ๐Ÿ“ Generate Docstrings
๐Ÿงช Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/volume-mount-api

๐Ÿ“œ Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 9663644 and 3b12c5d.

๐Ÿ“’ Files selected for processing (1)
  • src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt (1 hunks)
๐Ÿšง Files skipped from review as they are similar to previous changes (1)
  • src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test

Tip

๐Ÿ‘ฎ Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks โ€“ Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks โ€“ Define your own rules using CodeRabbitโ€™s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbitโ€™s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
src/test/kotlin/com/dcd/server/core/domain/application/service/CreateContainerServiceImplTest.kt (1)

24-37: ๋ชฉ ์„ธํŒ… ์ˆœ์„œ ์˜ค๋ฅ˜ ๋ฐ ๋„คํŠธ์›Œํฌ ๋ฌธ์ž์—ด ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ์ทจ์•ฝ/ํ”Œ๋ ˆ์ดํ‚ค ํ…Œ์ŠคํŠธ

  • Stubbing์ด ํ˜ธ์ถœ ์ดํ›„์— ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค(Line 26). ํ˜ธ์ถœ ์ „์— ์„ธํŒ…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณธ ์„œ๋น„์Šค๋Š” workspace.networkName์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ ๊ธฐ๋Œ€ ๋ฌธ์ž์—ด์€ title.replace(' ', '_')๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค(Line 31). ์‹ค์ œ ๊ตฌํ˜„๊ณผ ๋ถˆ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•ด ์ฃผ์„ธ์š”.

-        `when`("service๋ฅผ ์‹คํ–‰ํ• ๋•Œ") {
-            createContainerService.createContainer(application, application.externalPort)
-            every { queryVolumePort.findAllMountByApplication(application) } returns listOf()
+        `when`("service๋ฅผ ์‹คํ–‰ํ• ๋•Œ") {
+            every { queryVolumePort.findAllMountByApplication(application) } returns listOf()
+            createContainerService.createContainer(application, application.externalPort)
             then("์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ช…๋ น์„ ์‹คํ–‰ํ•ด์•ผํ•จ") {
                 verify {
                     commandPort.executeShellCommand(
-                        "docker create --network ${application.workspace.title.replace(' ', '_')} " +
+                        "docker create --network ${application.workspace.networkName} " +
                         "--name ${application.containerName} " +
                         "-p ${application.externalPort}:${application.port} ${application.containerName}:latest"
                     )
                 }
๐Ÿงน Nitpick comments (14)
src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt (2)

37-39: MockkBean ์ฃผ์ž…์€ ์ ์ ˆ. ๋‹ค๋งŒ ๋‹จ๊ณ„ ์ˆœ์„œ์™€ ์‹คํŒจ ๊ฒฝ๋กœ๋„ ํ•จ๊ป˜ ๋ณด์žฅํ•˜์„ธ์š”

  • ์ œ์•ˆ: ๋ฐฐํฌ ๋‹จ๊ณ„์˜ ํ˜ธ์ถœ ์ˆœ์„œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๊ฒ€์ฆํ•ด ํšŒ๊ท€๋ฅผ ๋ฐฉ์ง€ํ•˜์„ธ์š”.
  • ์ œ์•ˆ: ๋นŒ๋“œ ์‹คํŒจ ์‹œ createContainerService๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Œ์„ ๊ฒ€์ฆํ•˜๋Š” ๋„ค๊ฑฐํ‹ฐ๋ธŒ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

์˜ˆ์‹œ:

coVerifyOrder {
    commandPort.executeShellCommand("docker rm ${result.containerName}")
    commandPort.executeShellCommand("docker rmi ${result.containerName}")
    commandPort.executeShellCommand("git clone ${result.githubUrl} '${result.name}'")
    commandPort.executeShellCommand("cd ./'${result.name}' && docker build -t ${result.containerName}:latest .")
    createContainerService.createContainer(result, result.externalPort)
    commandPort.executeShellCommand("rm -rf '${result.name}'")
}

// ๋นŒ๋“œ ์‹คํŒจ ๋„ค๊ฑฐํ‹ฐ๋ธŒ ์ผ€์ด์Šค(๊ฐœ๋žต)
coEvery {
    commandPort.executeShellCommand("cd ./'${result.name}' && docker build -t ${result.containerName}:latest .")
} throws RuntimeException("build failed")

shouldThrow<Exception> {
    deployApplicationUseCase.execute(targetApplicationId)
}
coVerify(exactly = 0) { createContainerService.createContainer(any(), any()) }

์›ํ•˜์‹œ๋ฉด ์œ„ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํฌํ•จํ•œ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ํŒจ์น˜๋ฅผ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.


71-71: ์œ„์ž„ ํ˜ธ์ถœ์„ ๋‹จ 1ํšŒ๋กœ ์ œํ•œํ•ด ์ค‘๋ณต ์ƒ์„ฑ ํšŒ๊ท€ ๋ฐฉ์ง€

์ •ํ™•๋„ ํ–ฅ์ƒ์„ ์œ„ํ•ด ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ๋ช…์‹œํ•˜์„ธ์š”.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ • ๊ถŒ์žฅ:

-                coVerify { createContainerService.createContainer(result, result.externalPort) }
+                coVerify(exactly = 1) { createContainerService.createContainer(result, result.externalPort) }
src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt (1)

5-9: ์ž…๋ ฅ๋‹จ ์ตœ์†Œ ํŒจํ„ด ๊ฒ€์ฆ ์ถ”๊ฐ€ ์ œ์•ˆ

๋น„์ฆˆ๋‹ˆ์Šค ๋ ˆ์ด์–ด์—์„œ ์ตœ์ข… ๊ฒ€์ฆํ•˜๋”๋ผ๋„, ํ”„๋ฆฌ๋ฐธ๋ฆฌ๋ฐ์ด์…˜์œผ๋กœ ๊ฒฝ๋กœ ํŒจํ„ด์„ ์ œํ•œํ•˜๋ฉด UX์™€ ๋ณด์•ˆ์„ ๋™์‹œ์— ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ ์šฉ ์˜ˆ์‹œ:

 import jakarta.validation.constraints.NotBlank
+import jakarta.validation.constraints.Pattern

 data class MountVolumeRequest(
     @field:NotBlank
-    val mountPath: String,
+    @field:Pattern(regexp = "^(?:/[^\\s:]+)+$", message = "Invalid mount path")
+    val mountPath: String,
     val readOnly: Boolean,
 )
src/main/kotlin/com/dcd/server/persistence/volume/VolumePersistenceAdapter.kt (1)

31-33: ์ค‘๋ณต ๋งˆ์šดํŠธ ๋ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ ๋ฐฉ์•ˆ ํ•„์š”

๋‹จ์ˆœ save๋Š” (vol, app, mountPath) ์ค‘๋ณต ์‚ฝ์ž…์„ ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. DB ๋ ˆ๋ฒจ Unique ์ œ์•ฝ ๋ฐ/๋˜๋Š” upsert(idempotent) ์ฒ˜๋ฆฌ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„์—์„œ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€๋„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

์˜ˆ:

  • UNIQUE(volume_id, application_id, mount_path)
  • ์กด์žฌ ์‹œ ๋ฌด์‹œ/๊ฐฑ์‹  ์ •์ฑ… ์ •์˜
src/test/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCaseTest.kt (4)

31-33: ๋ถˆํ•„์š”ํ•œ MockkBean ์ œ๊ฑฐ ์ œ์•ˆ

@MockkBean CommandPort๋Š” ๋ณธ ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ถˆํ•„์š”ํ•œ ๋นˆ ์ฃผ์ž…์€ ์ปจํ…์ŠคํŠธ ๋กœ๋”ฉ ์‹œ๊ฐ„์„ ๋Š˜๋ฆฝ๋‹ˆ๋‹ค. ์ œ๊ฑฐ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

-    @MockkBean(relaxed = true)
-    private val commandPort: CommandPort,

53-57: ํ…Œ์ŠคํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์˜ค์—ผ ๋ฐฉ์ง€

findAll().size shouldBe 1 ๋‹จ์–ธ์€ ์ด์ „ ํ…Œ์ŠคํŠธ ์ž”์—ฌ ๋ฐ์ดํ„ฐ์— ๋ฏผ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์ปจํ…Œ์ด๋„ˆ/ํ…Œ์ŠคํŠธ ์ „์— volumeMountRepository.deleteAll()๋กœ ์ •๋ฆฌํ•˜์„ธ์š”.

         beforeContainer {
+            volumeMountRepository.deleteAll()
             val targetWorkspace = queryWorkspacePort.findById("d57b42f5-5cc4-440b-8dce-b4fc2e372eff")!!
             workspaceInfo.workspace = targetWorkspace
         }

Also applies to: 64-67


71-72: ID ํƒ€์ž… ์ผ๊ด€์„ฑ ๊ฒ€ํ† 

first.application.id.toString() ๋น„๊ต๋Š” ํƒ€์ž… ๋ถˆ์ผ์น˜์˜ ์‹ ํ˜ธ์ž…๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ID๋ฅผ ์ „ ๊ตฌ๊ฐ„(UUID โ†”๏ธŽ String)์—์„œ ์ผ๊ด€๋˜๊ฒŒ ๋‹ค๋ฃจ๋„๋ก ์ •๋ ฌ์„ ๊ฒ€ํ† ํ•ด์ฃผ์„ธ์š”(์˜ˆ: ์›น ๋ ˆ์ด์–ด๋Š” UUID ๋ฐ›๊ณ  ์œ ์Šค์ผ€์ด์Šค๋Š” UUID ์‚ฌ์šฉ).


89-94: ์˜คํƒˆ์ž ์ˆ˜์ •

"์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–‰ํ•ด์•ผํ•จ" โ†’ "์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผํ•จ".

-            then("์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–‰ํ•ด์•ผํ•จ") {
+            then("์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผํ•จ") {
src/test/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapterTest.kt (2)

56-56: MockK verifier์—์„œ ๋ถˆํ•„์š”ํ•œ ์บ์ŠคํŒ… ์ œ๊ฑฐ

any() as Type ๋Œ€์‹  ์ œ๋„ค๋ฆญ any<Type>() ์‚ฌ์šฉ์ด ๊ฐ„๊ฒฐํ•˜๊ณ  ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

-                verify { createVolumeUseCase.execute(any() as CreateVolumeReqDto) }
+                verify { createVolumeUseCase.execute(any<CreateVolumeReqDto>()) }
...
-                verify { updateVolumeUseCase.execute(testVolumeId, any() as UpdateVolumeReqDto) }
+                verify { updateVolumeUseCase.execute(testVolumeId, any<UpdateVolumeReqDto>()) }
...
-                verify { mountVolumeUseCase.execute(testVolumeId, testApplicationId, any() as MountVolumeReqDto) }
+                verify { mountVolumeUseCase.execute(testVolumeId, testApplicationId, any<MountVolumeReqDto>()) }

Also applies to: 91-91, 144-145


51-53: ํ…Œ์ŠคํŠธ ์„ค๋ช… ์˜คํƒˆ์ž ๋‹ค๋“ฌ๊ธฐ

  • "์‘๋‹ต๋˜์–ด์—ฌํ•จ" โ†’ "์‘๋‹ต๋˜์–ด์•ผํ•จ"
  • "๋ ˆํ•‘๋˜์„œ" โ†’ "๋ž˜ํ•‘๋˜์–ด"
-            then("์ƒํƒœ์ฝ”๋“œ๊ฐ€ OK๊ฐ€ ์‘๋‹ต๋˜์–ด์—ฌํ•จ") {
+            then("์ƒํƒœ์ฝ”๋“œ๊ฐ€ OK๊ฐ€ ์‘๋‹ต๋˜์–ด์•ผํ•จ") {
...
-            then("์œ ์Šค์ผ€์ด์Šค์˜ ์‘๋‹ต์ด ๋ ˆํ•‘๋˜์„œ ๋ฐ˜ํ™˜๋˜์–ด์•ผํ•จ") {
+            then("์œ ์Šค์ผ€์ด์Šค์˜ ์‘๋‹ต์ด ๋ž˜ํ•‘๋˜์–ด ๋ฐ˜ํ™˜๋˜์–ด์•ผํ•จ") {
...
-            then("์œ ์Šค์ผ€์ด์Šค์˜ ์‘๋‹ต์ด ๋ ˆํ•‘๋˜์„œ ๋ฐ˜ํ™˜๋˜์–ด์•ผํ•จ") {
+            then("์œ ์Šค์ผ€์ด์Šค์˜ ์‘๋‹ต์ด ๋ž˜ํ•‘๋˜์–ด ๋ฐ˜ํ™˜๋˜์–ด์•ผํ•จ") {

Also applies to: 106-108, 125-127

src/main/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapter.kt (2)

86-87: applicationId ์œ ํšจ์„ฑ ๊ฐ•ํ™”

์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์ตœ์†Œ ์œ ํšจ์„ฑ ๋ณด์žฅ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. (์˜ˆ: ๊ณต๋ฐฑ ๊ธˆ์ง€, UUID ํ˜•ํƒœ ๊ถŒ์žฅ) ํ˜„์žฌ๋Š” ๋นˆ ๋ฌธ์ž์—ด๋„ ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค.

+import jakarta.validation.constraints.NotBlank
...
-        @RequestParam applicationId: String,
+        @RequestParam @NotBlank applicationId: String,

์ถ”๊ฐ€๋กœ, ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด applicationId: UUID๋กœ ๋ฐ›๊ณ  ๋‚ด๋ถ€์—์„œ .toString() ๋ณ€ํ™˜ํ•˜๋„๋ก ์ผ๊ด€์„ฑ ํ™•๋ณด๋ฅผ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค.


88-90: ์ƒ์„ฑ์„ฑ ์ž‘์—…์˜ HTTP ์‘๋‹ต ์ฝ”๋“œ ์žฌ๊ฒ€ํ† 

๋งˆ์šดํŠธ ์ƒ์„ฑ์€ ์ž์› ์ƒ์„ฑ ์„ฑ๊ฒฉ์ž…๋‹ˆ๋‹ค. 200 OK ๋Œ€์‹  204 No Content ๋˜๋Š” 201 Created(+ Location ํ—ค๋”)๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”. ๊ธฐ์กด API ์ผ๊ด€์„ฑ ์œ ์ง€๊ฐ€ ์šฐ์„ ์ด๋ผ๋ฉด ๊ทธ๋Œ€๋กœ ๋‘ฌ๋„ ๋ฌด๋ฐฉํ•ฉ๋‹ˆ๋‹ค.

-        mountVolumeUseCase.execute(volumeId, applicationId, mountVolumeRequest.toDto())
-            .run { ResponseEntity.ok().build() }
+        mountVolumeUseCase.execute(volumeId, applicationId, mountVolumeRequest.toDto())
+            .run { ResponseEntity.noContent().build() }
src/main/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCase.kt (2)

25-47: ๋งˆ์šดํŠธ ๊ฒฝ๋กœ ๊ฒ€์ฆ ๋ฐ ์ค‘๋ณต ๋ฐฉ์ง€(๋ฉฑ๋“ฑ์„ฑ) ์ถ”๊ฐ€ ์ œ์•ˆ

  • ์ž…๋ ฅ ๊ฒ€์ฆ: ๋น„์–ด์žˆ์ง€ ์•Š๊ณ , ์ ˆ๋Œ€ ๊ฒฝ๋กœ(/ ์‹œ์ž‘), ๋ฃจํŠธ(/) ๊ธˆ์ง€, .. ๊ธˆ์ง€ ๋“ฑ ์ตœ์†Œ ๊ทœ์น™์„ ์œ ์Šค์ผ€์ด์Šค ๋ ˆ์ด์–ด์—์„œ ๊ฐ•์ œํ•˜์„ธ์š”. (๋„๋ฉ”์ธ init ๋Œ€์‹  ๋น„์ฆˆ๋‹ˆ์Šค ๋ ˆ์ด์–ด ์„ ํ˜ธ โ€” ๊ณผ๊ฑฐ ๋Ÿฌ๋‹๊ณผ ์ผ์น˜)
  • ์ค‘๋ณต ๋ฐฉ์ง€: ๋™์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋™์ผ mountPath ์ค‘๋ณต ์ƒ์„ฑ ๋ฐฉ์ง€(์„ ์  ์ฒดํฌ + DB ์œ ๋‹ˆํฌ ํ‚ค).
 class MountVolumeUseCase(
@@
 ) {
     fun execute(volumeId: UUID, applicationId: String, mountVolumeReqDto: MountVolumeReqDto) {
+        require(mountVolumeReqDto.mountPath.isNotBlank()) { "mountPath must not be blank" }
+        require(mountVolumeReqDto.mountPath.startsWith("/") && mountVolumeReqDto.mountPath.length > 1) {
+            "mountPath must be an absolute, non-root path"
+        }
+        require(!mountVolumeReqDto.mountPath.contains("..")) { "mountPath must not contain '..'" }
@@
-        val volumeMount = VolumeMount(
+        // TODO: ์ค‘๋ณต ์กด์žฌ ์‹œ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ(๋ฌด์‹œ ๋˜๋Š” AlreadyExists ์˜ˆ์™ธ)
+        val volumeMount = VolumeMount(
             id = UUID.randomUUID(),
             application = application,
             volume = volume,
             mountPath = mountVolumeReqDto.mountPath,
             readOnly = mountVolumeReqDto.readOnly
         )

DB ์ธก๋ฉด์—์„œ๋Š” (application_id, mount_path) ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.


49-51: ์ด๋ฒคํŠธ ๋ฐœํ–‰ ํƒ€์ด๋ฐ(ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ํ›„) ํ™•์ธ

ํ˜„์žฌ ์ฆ‰์‹œ ๋ฐœํ–‰์ž…๋‹ˆ๋‹ค. ๋ฆฌ์Šค๋„ˆ๊ฐ€ @TransactionalEventListener(phase = AFTER_COMMIT)๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด, ์ปค๋ฐ‹ ์‹คํŒจ ์‹œ ๋ถˆ์ผ์น˜๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปค๋ฐ‹ ์ดํ›„ ๋ฐœํ–‰์„ ๋ณด์žฅํ•˜๋„๋ก ๋ฆฌ์Šค๋„ˆ ์„ค์ •์„ ํ™•์ธํ•˜๊ฑฐ๋‚˜, after-commit ํ›…์„ ์‚ฌ์šฉํ•˜์„ธ์š”.

์˜ˆ์ƒ ์กฐ์น˜:

  • ๋ฆฌ์Šค๋„ˆ์— @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) ์ ์šฉ ์—ฌ๋ถ€ ํ™•์ธ
  • ๋˜๋Š” TransactionSynchronizationManager.registerSynchronization(...afterCommit { publish }) ์‚ฌ์šฉ
๐Ÿ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 9e623ff and 9663644.

๐Ÿ“’ Files selected for processing (13)
  • src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateContainerServiceImpl.kt (1 hunks)
  • src/main/kotlin/com/dcd/server/core/domain/volume/dto/request/MountVolumeReqDto.kt (1 hunks)
  • src/main/kotlin/com/dcd/server/core/domain/volume/spi/CommandVolumePort.kt (1 hunks)
  • src/main/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCase.kt (1 hunks)
  • src/main/kotlin/com/dcd/server/infrastructure/global/config/SecurityConfig.kt (1 hunks)
  • src/main/kotlin/com/dcd/server/persistence/volume/VolumePersistenceAdapter.kt (1 hunks)
  • src/main/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapter.kt (4 hunks)
  • src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeRequestExtension.kt (2 hunks)
  • src/main/kotlin/com/dcd/server/presentation/domain/volume/data/request/MountVolumeRequest.kt (1 hunks)
  • src/test/kotlin/com/dcd/server/core/domain/application/service/CreateContainerServiceImplTest.kt (1 hunks)
  • src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt (4 hunks)
  • src/test/kotlin/com/dcd/server/core/domain/volume/usecase/MountVolumeUseCaseTest.kt (1 hunks)
  • src/test/kotlin/com/dcd/server/presentation/domain/volume/VolumeWebAdapterTest.kt (4 hunks)
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (2)
๐Ÿ““ Common learnings
Learnt from: dolong2
PR: dolong2/dcd#703
File: src/main/kotlin/com/dcd/server/core/domain/volume/model/Volume.kt:1-12
Timestamp: 2025-09-01T14:52:42.625Z
Learning: dolong2 prefers to implement validation logic (such as path validation for Volume.physicalPath) in the business logic layer rather than in domain model init blocks, keeping domain models as simple data containers.
๐Ÿ“š Learning: 2025-09-04T14:59:15.319Z
Learnt from: dolong2
PR: dolong2/dcd#710
File: src/main/kotlin/com/dcd/server/core/domain/volume/service/impl/DeleteDockerVolumeServiceImpl.kt:13-18
Timestamp: 2025-09-04T14:59:15.319Z
Learning: dolong2 addressed command injection vulnerability in volume deletion by adding validation in the volume creation service (CreateDockerVolumeServiceImpl) using NAME_SAFE_REGEX pattern, preferring to validate at the source rather than in the deletion service for better security and fail-fast behavior.

Applied to files:

  • src/main/kotlin/com/dcd/server/core/domain/application/service/impl/CreateContainerServiceImpl.kt
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test
๐Ÿ”‡ Additional comments (6)
src/test/kotlin/com/dcd/server/core/domain/application/usecase/DeployApplicationUseCaseTest.kt (2)

7-7: CreateContainerService ์˜์กด์„ฑ ์ž„ํฌํŠธ ์ ์ ˆ

์œ ์Šค์ผ€์ด์Šค ์œ„์ž„์„ ํ…Œ์ŠคํŠธ๋กœ ๋ณด์žฅํ•˜๋Š” ๋ฐฉํ–ฅ์ด ๋งž์Šต๋‹ˆ๋‹ค.


22-22: ํ•ด๋‹น ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ์ฝ”๋ฉ˜ํŠธ ์ƒ๋žต

์ •์  ๋ถ„์„์—์„œ ๊ฒฝ๊ณ ๊ฐ€ ์—†๋‹ค๋ฉด ์ถ”๊ฐ€ ์กฐ์น˜ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

src/main/kotlin/com/dcd/server/infrastructure/global/config/SecurityConfig.kt (1)

107-107: ๋งˆ์šดํŠธ ์—”๋“œํฌ์ธํŠธ ์ธ์ฆ ์ถ”๊ฐ€ ์ ์ ˆ (๊ธฐ์กด ์ •์ฑ…๊ณผ ์ผ๊ด€). ๊ถŒํ•œ ๋ชจ๋ธ์€ ์ถ”ํ›„ ์ ๊ฒ€ ๊ถŒ์žฅ

๋‹ค๋ฅธ ๋ณผ๋ฅจ API์™€ ๋™์ผํ•˜๊ฒŒ authenticated๋กœ ๋งž์ถ˜ ์  OK. ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฉค๋ฒ„์‹ญ/ROLE ๊ธฐ๋ฐ˜ ์„ธ๋ถ„ ๊ถŒํ•œ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์ถ”ํ›„ ์ธ๊ฐ€ ๊ทœ์น™ ํ™•์žฅ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”.

ํ˜„์žฌ๋Š” ์ธ์ฆ๋งŒ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ ๊ถŒํ•œ ์ฒดํฌ๊ฐ€ ๋ณ„๋„ ํ•„ํ„ฐ/์–ด๋“œ๋ฐ”์ด์ €๋กœ ์ด๋ฏธ ์ ์šฉ๋˜๋Š”์ง€ ํ™•์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

src/main/kotlin/com/dcd/server/core/domain/volume/spi/CommandVolumePort.kt (2)

4-4: ์ƒˆ API ์ถ”๊ฐ€(LGTM) โ€” ๋„๋ฉ”์ธ ๋ชจ๋ธ ์˜์กด์„ฑ ์ ์ ˆ

VolumeMount๋ฅผ ํฌํŠธ์— ๋…ธ์ถœํ•˜๋Š” ๊ฒฐ์ •์€ ํ•ฉ๋ฆฌ์ ์ž…๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ์ค‘์‹ฌ ํ๋ฆ„๊ณผ ์ผ๊ด€๋ฉ๋‹ˆ๋‹ค.


11-11: ๋ชจ๋“  ๊ตฌํ˜„์ฒด ๋™๊ธฐํ™” ํ™•์ธ โ€” saveMount ๊ตฌํ˜„ ๋ฐ˜์˜ ์—ฌ๋ถ€
rg ์‹คํ–‰ ๊ฒฐ๊ณผ saveMount๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•œ ๊ตฌํ˜„์ฒด๋Š” src/main/kotlin/com/dcd/server/persistence/volume/VolumePersistenceAdapter.kt:31 ํ•œ ๊ณณ๋ฟ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์–ด๋Œ‘ํ„ฐ(ํƒ€ ๋ชจ๋“ˆ ํฌํ•จ)๊ฐ€ ์žˆ๋‹ค๋ฉด ๋™์ผ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋กœ ๊ตฌํ˜„์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

src/main/kotlin/com/dcd/server/presentation/domain/volume/data/extension/VolumeRequestExtension.kt (1)

22-26: ๋งคํ•‘ ํ•จ์ˆ˜ ์ ์ ˆ (๋ณ€ํ™˜ ๋ˆ„๋ฝ ์—†์Œ)

ํ•„๋“œ 1:1 ๋งคํ•‘์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

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

Labels

1๏ธโƒฃ Priority: ์ƒ ์šฐ์„ ์ˆœ์œ„ ์ƒ โœจ Feature ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ

Projects

None yet

Development

Successfully merging this pull request may close these issues.

๋ณผ๋ฅจ ๋งˆ์šดํŠธ API ์ถ”๊ฐ€

2 participants