Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.process.clash.adapter.persistence.helpcontent;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "help_contents")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class HelpContentJpaEntity {

@Id
@Column(nullable = false, updatable = false)
private String key;

@Column(nullable = false, columnDefinition = "TEXT")
private String content;

@Version
@Column(nullable = false)
private Long version;

@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;

@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.process.clash.adapter.persistence.helpcontent;

import com.process.clash.domain.helpcontent.entity.HelpContent;
import org.springframework.stereotype.Component;

@Component
public class HelpContentJpaMapper {

public HelpContent toDomain(HelpContentJpaEntity entity) {
return new HelpContent(
entity.getKey(),
entity.getContent(),
entity.getVersion(),
entity.getCreatedAt(),
entity.getUpdatedAt()
);
}

public HelpContentJpaEntity toJpaEntity(HelpContent domain) {
return new HelpContentJpaEntity(
domain.key(),
domain.content(),
domain.version(),
domain.createdAt(),
domain.updatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.process.clash.adapter.persistence.helpcontent;

import org.springframework.data.jpa.repository.JpaRepository;

public interface HelpContentJpaRepository extends JpaRepository<HelpContentJpaEntity, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.process.clash.adapter.persistence.helpcontent;

import com.process.clash.application.helpcontent.port.out.HelpContentRepositoryPort;
import com.process.clash.domain.helpcontent.entity.HelpContent;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
@RequiredArgsConstructor
public class HelpContentPersistenceAdapter implements HelpContentRepositoryPort {

private final HelpContentJpaRepository helpContentJpaRepository;
private final HelpContentJpaMapper helpContentJpaMapper;

@Override
public Optional<HelpContent> findByKey(String key) {
return helpContentJpaRepository.findById(key).map(helpContentJpaMapper::toDomain);
}

@Override
public boolean existsByKey(String key) {
return helpContentJpaRepository.existsById(key);
}

@Override
public HelpContent save(HelpContent helpContent) {
HelpContentJpaEntity entity = helpContentJpaMapper.toJpaEntity(helpContent);
HelpContentJpaEntity saved = helpContentJpaRepository.saveAndFlush(entity);
return helpContentJpaMapper.toDomain(saved);
}
Comment thread
chaeyn marked this conversation as resolved.
Resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.process.clash.adapter.web.helpcontent.admin.controller;

import com.process.clash.adapter.web.common.ApiResponse;
import com.process.clash.adapter.web.helpcontent.docs.controller.AdminHelpContentControllerDocument;
import com.process.clash.adapter.web.helpcontent.dto.CreateHelpContentDto;
import com.process.clash.adapter.web.helpcontent.dto.UpdateHelpContentDto;
import com.process.clash.adapter.web.security.AuthenticatedActor;
import com.process.clash.application.common.actor.Actor;
import com.process.clash.application.helpcontent.data.CreateHelpContentData;
import com.process.clash.application.helpcontent.data.UpdateHelpContentData;
import com.process.clash.application.helpcontent.port.in.CreateHelpContentUseCase;
import com.process.clash.application.helpcontent.port.in.UpdateHelpContentUseCase;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/admin/help-contents")
@RequiredArgsConstructor
public class AdminHelpContentController implements AdminHelpContentControllerDocument {

private final CreateHelpContentUseCase createHelpContentUseCase;
private final UpdateHelpContentUseCase updateHelpContentUseCase;

@Override
@PostMapping
public ApiResponse<CreateHelpContentDto.Response> createHelpContent(
@AuthenticatedActor Actor actor,
@Valid @RequestBody CreateHelpContentDto.Request request
) {
CreateHelpContentData.Result result = createHelpContentUseCase.execute(request.toCommand(actor));
return ApiResponse.created(
CreateHelpContentDto.Response.from(result),
"도움말 내용이 성공적으로 생성되었습니다."
);
}

@Override
@PutMapping("/{key}")
public ApiResponse<UpdateHelpContentDto.Response> updateHelpContent(
@AuthenticatedActor Actor actor,
@PathVariable String key,
@Valid @RequestBody UpdateHelpContentDto.Request request
) {
UpdateHelpContentData.Result result = updateHelpContentUseCase.execute(request.toCommand(actor, key));
return ApiResponse.success(
UpdateHelpContentDto.Response.from(result),
"도움말 내용이 성공적으로 수정되었습니다."
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.process.clash.adapter.web.helpcontent.controller;

import com.process.clash.adapter.web.helpcontent.docs.controller.HelpContentControllerDocument;
import com.process.clash.application.helpcontent.data.GetHelpContentData;
import com.process.clash.application.helpcontent.port.in.GetHelpContentUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;

import java.nio.charset.StandardCharsets;

@RestController
@RequestMapping("/api/help-contents")
@RequiredArgsConstructor
public class HelpContentController implements HelpContentControllerDocument {

private final GetHelpContentUseCase getHelpContentUseCase;

@Override
@GetMapping("/{key}")
public ResponseEntity<String> getHelpContent(
Comment thread
chaeyn marked this conversation as resolved.
Resolved
@PathVariable String key,
WebRequest request
) {
GetHelpContentData.Result result = getHelpContentUseCase.execute(key);
String etag = "\"" + result.version() + "\"";

if (request.checkNotModified(etag)) {
return null;
}

return ResponseEntity.ok()
.contentType(new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8))
.cacheControl(CacheControl.noCache())
.eTag(etag)
.body(result.content());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.process.clash.adapter.web.helpcontent.docs.controller;

import com.process.clash.adapter.web.common.ApiResponse;
import com.process.clash.adapter.web.helpcontent.dto.CreateHelpContentDto;
import com.process.clash.adapter.web.helpcontent.dto.UpdateHelpContentDto;
import com.process.clash.application.common.actor.Actor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PathVariable;

@Tag(name = "어드민 도움말 API", description = "클라이언트 도움말 수정")
public interface AdminHelpContentControllerDocument {

@Operation(summary = "도움말 생성", description = "새 도움말 키와 전체 텍스트를 생성합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "생성 성공",
content = @Content(
schema = @Schema(implementation = CreateHelpContentDto.Response.class),
examples = @ExampleObject(value = """
{
"success": true,
"message": "도움말 내용이 성공적으로 생성되었습니다.",
"data": {
"key": "new-tooltip",
"content": "새 안내 문구입니다.",
"version": 0,
"createdAt": "2026-07-14T14:00:00Z"
}
}
""")
)),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 존재하는 도움말 키")
})
ApiResponse<CreateHelpContentDto.Response> createHelpContent(
@Parameter(hidden = true) Actor actor,
CreateHelpContentDto.Request request
);

@Operation(summary = "도움말 수정", description = "도움말 전체 텍스트를 수정하고 ETag 버전을 증가시킵니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "수정 성공",
content = @Content(
schema = @Schema(implementation = UpdateHelpContentDto.Response.class),
examples = @ExampleObject(value = """
{
"success": true,
"message": "도움말 내용이 성공적으로 수정되었습니다.",
"data": {
"key": "cookie-tooltip",
"content": "쿠키는 상점에서 사용 할 수 있어요!",
"version": 2,
"updatedAt": "2026-07-14T14:00:00Z"
}
}
""")
)),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "도움말을 찾을 수 없음")
})
ApiResponse<UpdateHelpContentDto.Response> updateHelpContent(
@Parameter(hidden = true) Actor actor,
@Parameter(description = "수정할 도움말 키", example = "cookie-tooltip", required = true) @PathVariable String key,
UpdateHelpContentDto.Request request
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.process.clash.adapter.web.helpcontent.docs.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.context.request.WebRequest;

@Tag(name = "도움말 API", description = "클라이언트 도움말 조회")
public interface HelpContentControllerDocument {

@Operation(summary = "도움말 조회", description = "If-None-Match 헤더가 현재 ETag와 같으면 304를, 다르면 도움말 전체 텍스트와 ETag를 반환합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공",
content = @Content(
mediaType = "text/plain",
schema = @Schema(type = "string"),
examples = @ExampleObject(value = "쿠키는 상점에서 사용 할 수 있어요!\\n\\n로드맵 챕터 클리어: 100 쿠키")
)),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "304", description = "변경 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "도움말을 찾을 수 없음")
})
ResponseEntity<String> getHelpContent(@PathVariable String key, WebRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.process.clash.adapter.web.helpcontent.dto;

import com.process.clash.application.common.actor.Actor;
import com.process.clash.application.helpcontent.data.CreateHelpContentData;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

import java.time.Instant;

public class CreateHelpContentDto {

@Schema(name = "CreateHelpContentDtoRequest")
public record Request(
@NotBlank(message = "도움말 키는 비워둘 수 없습니다.")
@Pattern(regexp = "[a-z0-9-]+", message = "도움말 키는 영문 소문자, 숫자, 하이픈만 사용할 수 있습니다.")
String key,

@NotBlank(message = "도움말 내용은 비워둘 수 없습니다.")
String content
) {
public CreateHelpContentData.Command toCommand(Actor actor) {
return new CreateHelpContentData.Command(actor, key, content);
}
}

@Schema(name = "CreateHelpContentDtoResponse")
public record Response(String key, String content, long version, Instant createdAt) {
public static Response from(CreateHelpContentData.Result result) {
return new Response(result.key(), result.content(), result.version(), result.createdAt());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.process.clash.adapter.web.helpcontent.dto;

import com.process.clash.application.common.actor.Actor;
import com.process.clash.application.helpcontent.data.UpdateHelpContentData;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

import java.time.Instant;

public class UpdateHelpContentDto {

@Schema(name = "UpdateHelpContentDtoRequest")
public record Request(
@NotBlank(message = "도움말 내용은 비워둘 수 없습니다.")
String content
) {
public UpdateHelpContentData.Command toCommand(Actor actor, String key) {
return new UpdateHelpContentData.Command(actor, key, content);
}
}

@Schema(name = "UpdateHelpContentDtoResponse")
public record Response(String key, String content, long version, Instant updatedAt) {
public static Response from(UpdateHelpContentData.Result result) {
return new Response(result.key(), result.content(), result.version(), result.updatedAt());
}
}
}
Loading
Loading