Skip to content
Open
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
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
FROM eclipse-temurin:21-jre
FROM eclipse-temurin:25-jre
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

현재 Base 이미지로 eclipse-temurin:25-jre를 사용하도록 변경되었습니다. Java 25는 아직 정식 출시(LTS)되지 않은 버전이므로, Docker Hub에 해당 태그의 이미지가 존재하지 않거나 불안정할 수 있어 빌드 및 배포가 실패할 것입니다. 기존에 사용하던 안정적인 LTS 버전인 eclipse-temurin:21-jre를 계속 사용하는 것을 권장합니다.

FROM eclipse-temurin:21-jre


WORKDIR /app

COPY build/libs/*.jar app.jar
COPY app.jar app.jar

EXPOSE 8080

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.tokenledgercloud.api.domain.pricing.controller;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.tokenledgercloud.api.domain.pricing.entity.PricingCatalog;
import com.tokenledgercloud.api.domain.pricing.service.PricingCatalogService;
import com.tokenledgercloud.api.global.exception.PricingCatalogGenerationFailedException;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/pricing-catalogs")
public class PricingCatalogController {

private static final String YAML_MEDIA_TYPE = "application/x-yaml";

private final PricingCatalogService pricingCatalogService;

@GetMapping(
value = "/default.yml",
produces = YAML_MEDIA_TYPE
)
public ResponseEntity<String> getDefaultYaml(
@RequestParam(required = false) String version,
@RequestParam(required = false) String checksum,
@RequestHeader(value = HttpHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch
) {
PricingCatalog catalog = pricingCatalogService.getDefaultCatalog(version);

if (catalog.getGeneratedYaml() == null || catalog.getGeneratedYaml().isBlank()) {
throw new PricingCatalogGenerationFailedException();
}

String etag = toEtag(catalog.getChecksum());

if (etag.equals(ifNoneMatch) || catalog.getChecksum().equals(checksum) || etag.equals(checksum)) {
return ResponseEntity.status(304)
.eTag(etag)
.cacheControl(CacheControl.noCache())
.build();
}

return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(YAML_MEDIA_TYPE))
.header(HttpHeaders.ETAG, etag)
.header(HttpHeaders.LAST_MODIFIED, toHttpDate(catalog))
.cacheControl(CacheControl.noCache())
.body(catalog.getGeneratedYaml());
}

private String toEtag(String checksum) {
return "\"" + checksum + "\"";
}

private String toHttpDate(PricingCatalog catalog) {
LocalDateTime lastModified = catalog.getPublishedAt();

if (lastModified == null) {
lastModified = catalog.getUpdatedAt();
}

if (lastModified == null) {
lastModified = catalog.getCreatedAt();
}

if (lastModified == null) {
lastModified = LocalDateTime.now(ZoneOffset.UTC);
}

return lastModified
.atZone(ZoneOffset.UTC)
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.tokenledgercloud.api.domain.pricing.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.tokenledgercloud.api.domain.pricing.dto.PricingPlanCreateRequest;
import com.tokenledgercloud.api.domain.pricing.dto.PricingPlanCreateResponse;
import com.tokenledgercloud.api.domain.pricing.dto.PricingPlanListResponse;
import com.tokenledgercloud.api.domain.pricing.service.PricingPlanService;
import com.tokenledgercloud.api.global.response.ApiResponse;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/pricing-plans")
public class PricingPlanController {

private final PricingPlanService pricingPlanService;

@GetMapping
public ResponseEntity<ApiResponse<PricingPlanListResponse>> getPricingPlans(
@RequestParam(required = false) String provider,
@RequestParam(required = false) String model,
@RequestParam(required = false, defaultValue = "false") Boolean activeOnly
) {
return ResponseEntity.ok(
ApiResponse.success(
"가격표 목록 조회 성공",
pricingPlanService.getPricingPlans(provider, model, activeOnly)
)
);
}

@PostMapping
public ResponseEntity<ApiResponse<PricingPlanCreateResponse>> create(
@Valid @RequestBody PricingPlanCreateRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(
"가격표 등록 성공",
pricingPlanService.create(request)
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.tokenledgercloud.api.domain.pricing.dto;

import java.math.BigDecimal;
import java.time.OffsetDateTime;

import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record PricingPlanCreateRequest(
@NotBlank String provider,
@NotBlank String model,
@NotBlank String currency,

@NotNull
@DecimalMin("0.000000")
BigDecimal promptRate,

@NotNull
@DecimalMin("0.000000")
BigDecimal completionRate,

@DecimalMin("0.000000")
BigDecimal reasoningRate,

@DecimalMin("0.000000")
BigDecimal cachedPromptRate,

@NotBlank String version,

@NotNull OffsetDateTime effectiveFrom,

OffsetDateTime effectiveTo
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.tokenledgercloud.api.domain.pricing.dto;

public record PricingPlanCreateResponse(
String pricingPlanId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.tokenledgercloud.api.domain.pricing.dto;

import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

import com.tokenledgercloud.api.domain.pricing.entity.PricingPlan;

public record PricingPlanItemResponse(
String pricingPlanId,
String provider,
String model,
String currency,
BigDecimal promptRate,
BigDecimal completionRate,
BigDecimal reasoningRate,
String version,
OffsetDateTime effectiveFrom,
OffsetDateTime effectiveTo
) {
public static PricingPlanItemResponse from(PricingPlan plan) {
return new PricingPlanItemResponse(
plan.getId(),
plan.getProvider(),
plan.getModel(),
plan.getCurrency(),
plan.getPromptRate(),
plan.getCompletionRate(),
plan.getReasoningRate(),
plan.getVersion(),
toUtcOffsetDateTime(plan.getEffectiveFrom()),
toUtcOffsetDateTime(plan.getEffectiveTo())
);
}

private static OffsetDateTime toUtcOffsetDateTime(java.time.LocalDateTime value) {
return value == null ? null : value.atOffset(ZoneOffset.UTC);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.tokenledgercloud.api.domain.pricing.dto;

import java.util.List;

public record PricingPlanListResponse(
List<PricingPlanItemResponse> items
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.tokenledgercloud.api.domain.pricing.entity;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.UUID;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "pricing_catalogs")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PricingCatalog {

@Id
@Column(length = 36)
private String id;

@Column(name = "catalog_key", nullable = false, length = 50)
private String catalogKey;

@Column(nullable = false, length = 50)
private String version;

@Column(nullable = false, length = 20)
private String format;

@Column(nullable = false, length = 128)
private String checksum;

@Column(name = "is_active", nullable = false)
private Boolean isActive;

@Lob
@Column(name = "generated_yaml", nullable = false)
private String generatedYaml;

@Column(name = "published_at")
private LocalDateTime publishedAt;

@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;

@PrePersist
void prePersist() {
if (id == null || id.isBlank()) {
id = UUID.randomUUID().toString();
}

if (format == null) {
format = "yaml";
}

if (isActive == null) {
isActive = true;
}

LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);

if (createdAt == null) {
createdAt = now;
}

if (updatedAt == null) {
updatedAt = now;
}
}

@PreUpdate
void preUpdate() {
updatedAt = LocalDateTime.now(ZoneOffset.UTC);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.tokenledgercloud.api.domain.pricing.entity;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "pricing_plans")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PricingPlan {

@Id
@Column(length = 36)
private String id;

@Column(name = "catalog_id", length = 36)
private String catalogId;

@Column(nullable = false, length = 50)
private String provider;

@Column(nullable = false, length = 100)
private String model;

@Column(nullable = false, length = 10)
private String currency;

@Column(name = "prompt_rate", nullable = false, precision = 18, scale = 6)
private BigDecimal promptRate;

@Column(name = "completion_rate", nullable = false, precision = 18, scale = 6)
private BigDecimal completionRate;

@Column(name = "reasoning_rate", precision = 18, scale = 6)
private BigDecimal reasoningRate;

@Column(name = "cached_prompt_rate", precision = 18, scale = 6)
private BigDecimal cachedPromptRate;

@Column(nullable = false, length = 50)
private String version;

@Column(name = "effective_from", nullable = false)
private LocalDateTime effectiveFrom;

@Column(name = "effective_to")
private LocalDateTime effectiveTo;

@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

@PrePersist
void prePersist() {
if (id == null || id.isBlank()) {
id = UUID.randomUUID().toString();
}
if (createdAt == null) {
createdAt = LocalDateTime.now(ZoneOffset.UTC);
}
if (reasoningRate == null) {
reasoningRate = BigDecimal.ZERO;
}
if (cachedPromptRate == null) {
cachedPromptRate = BigDecimal.ZERO;
}
}
}
Loading