diff --git a/build.gradle b/build.gradle index e33aaa9f..44ca2390 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ tasks.named('test') { } asciidoctor.doFirst { - delete file('src/main/resources/static/docs') + delete file('src/main/resources/docs') } asciidoctor { @@ -94,7 +94,7 @@ task createDocument(type: Copy) { dependsOn asciidoctor from asciidoctor.outputDir - into file("src/main/resources/static") + into file("src/main/resources/docs") } bootJar { diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/config/RedisStreamConfig.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/config/RedisStreamConfig.java new file mode 100644 index 00000000..898b2d17 --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/consumer/config/RedisStreamConfig.java @@ -0,0 +1,43 @@ +package com.wootecam.luckyvickyauction.consumer.config; + +import java.util.UUID; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; + +@Configuration +public class RedisStreamConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private String redisPort; + + @Value("${spring.data.redis.password}") + private String redisPassword; + + @Getter + @Value("${stream.key}") + private String streamKey; + @Getter + @Value("${stream.consumer.groupName}") + private String consumerGroupName; + @Getter + private String consumerName = UUID.randomUUID().toString(); + + // todo [추후 서버를 분리하면 모듈이 분리될 상황을 가정한 빈입니다.] [2024-08-23] [yudonggeun] +// @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(redisHost); + redisStandaloneConfiguration.setPort(Integer.parseInt(redisPort)); + redisStandaloneConfiguration.setPassword(redisPassword); + + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + +} diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/PendingMessageConsumer.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/PendingMessageConsumer.java new file mode 100644 index 00000000..4a4d8985 --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/PendingMessageConsumer.java @@ -0,0 +1,56 @@ +package com.wootecam.luckyvickyauction.consumer.presentation; + +import com.wootecam.luckyvickyauction.consumer.config.RedisStreamConfig; +import java.time.Duration; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.PendingMessage; +import org.springframework.data.redis.connection.stream.PendingMessages; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@EnableScheduling +@Component +@RequiredArgsConstructor +public class PendingMessageConsumer { + + private final RedisOperator redisOperator; + private final RedisStreamConfig redisStreamConfig; + + @Scheduled(fixedRate = 1000) + public void consumePendingMessage() { + + // 처리되지 않은 메시지 조회 + PendingMessages pendingMessageInfos = redisOperator.getPendingMessage( + redisStreamConfig.getStreamKey(), + redisStreamConfig.getConsumerGroupName(), + redisStreamConfig.getConsumerName() + ); + + RecordId[] recordIds = pendingMessageInfos.stream().map( + PendingMessage::getId + ).toArray(RecordId[]::new); + + // 처리되지 않은 메시지 데이터 조회 + List> messages = redisOperator.claim( + redisStreamConfig.getStreamKey(), + redisStreamConfig.getConsumerGroupName(), + redisStreamConfig.getConsumerName(), + Duration.ofMinutes(1), + recordIds + ); + + // 메시지 처리 + messages.forEach(message -> { + log.info("MessageId: {}", message.getId()); + log.info("Stream: {}", message.getStream()); + log.info("Body: {}", message.getValue()); + redisOperator.acknowledge(redisStreamConfig.getConsumerGroupName(), message); + }); + } +} diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisOperator.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisOperator.java new file mode 100644 index 00000000..97cf67da --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisOperator.java @@ -0,0 +1,113 @@ +package com.wootecam.luckyvickyauction.consumer.presentation; + +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.output.StatusOutput; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandKeyword; +import io.lettuce.core.protocol.CommandType; +import java.time.Duration; +import java.util.Iterator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.PendingMessages; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamInfo; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RedisOperator { + + private static final Logger log = LoggerFactory.getLogger(RedisOperator.class); + private final RedisConnectionFactory redisConnectionFactory; + private final StringRedisTemplate redisTemplate; + + + public boolean isStreamConsumerGroupExist(String streamKey, String consumerGroupName) { + Iterator iterator = this.redisTemplate + .opsForStream().groups(streamKey).stream().iterator(); + + while (iterator.hasNext()) { + StreamInfo.XInfoGroup xInfoGroup = iterator.next(); + if (xInfoGroup.groupName().equals(consumerGroupName)) { + return true; + } + } + return false; + } + + public void createStreamConsumerGroup(String streamKey, String consumerGroupName) { + // if stream is not exist, create stream and consumer group of it + if (Boolean.FALSE.equals(this.redisTemplate.hasKey(streamKey))) { + RedisAsyncCommands commands = (RedisAsyncCommands) redisConnectionFactory + .getConnection() + .getNativeConnection(); + + CommandArgs args = new CommandArgs<>(StringCodec.UTF8) + .add(CommandKeyword.CREATE) + .add(streamKey) + .add(consumerGroupName) + .add("0") + .add("MKSTREAM"); + + commands.dispatch(CommandType.XGROUP, new StatusOutput(StringCodec.UTF8), args); + } + // stream is exist, create consumerGroup if is not exist + else { + if (!isStreamConsumerGroupExist(streamKey, consumerGroupName)) { + this.redisTemplate.opsForStream().createGroup(streamKey, ReadOffset.from("0"), consumerGroupName); + } + } + } + + public StreamMessageListenerContainer createStreamMessageListenerContainer() { + return StreamMessageListenerContainer.create(redisConnectionFactory, + StreamMessageListenerContainer + .StreamMessageListenerContainerOptions.builder() + .hashKeySerializer(new StringRedisSerializer()) + .hashValueSerializer(new StringRedisSerializer()) + .pollTimeout(Duration.ofMillis(20)) + .build() + ); + } + + public void acknowledge(String consumerGroup, MapRecord message) { + Long ack = this.redisTemplate.opsForStream().acknowledge(consumerGroup, message); + if (ack == 0) { + log.error("Acknowledge failed. MessageId: {}", message.getId()); + } else { + log.info("Acknowledge success. MessageId: {}", message.getId()); + } + } + + public PendingMessages getPendingMessage(String streamKey, String consumerGroup, String consumerName) { + return this.redisTemplate.opsForStream() + .pending(streamKey, + Consumer.from(consumerGroup, consumerName), + Range.unbounded(), + 100L + ); + } + + public List> claim(String streamKey, + String consumerGroup, String consumerName, + Duration minIdleTime, RecordId... messageIds) { + if (messageIds.length < 1) { + return List.of(); + } + + return this.redisTemplate.opsForStream() + .claim(streamKey, consumerGroup, consumerName, minIdleTime, messageIds); + } +} diff --git a/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisStreamConsumer.java b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisStreamConsumer.java new file mode 100644 index 00000000..bab5798e --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/consumer/presentation/RedisStreamConsumer.java @@ -0,0 +1,75 @@ +package com.wootecam.luckyvickyauction.consumer.presentation; + +import com.wootecam.luckyvickyauction.consumer.config.RedisStreamConfig; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.stream.StreamListener; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; +import org.springframework.data.redis.stream.Subscription; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisStreamConsumer implements StreamListener> { + + private final RedisOperator redisOperator; + private final RedisStreamConfig redisStreamConfig; + private StreamMessageListenerContainer> listenerContainer; + private Subscription subscription; + + @Override + public void onMessage(MapRecord message) { + log.info("MessageId: {}", message.getId()); + log.info("Stream: {}", message.getStream()); + log.info("Body: {}", message.getValue()); + redisOperator.acknowledge(redisStreamConfig.getConsumerGroupName(), message); + } + + @PostConstruct + public void init() throws InterruptedException { + // Consumer Group 설정 + this.redisOperator.createStreamConsumerGroup( + redisStreamConfig.getStreamKey(), + redisStreamConfig.getConsumerGroupName()); + + // StreamMessageListenerContainer 설정 + this.listenerContainer = this.redisOperator.createStreamMessageListenerContainer(); + + //Subscription 설정 + this.subscription = this.listenerContainer.receive( + Consumer.from( + redisStreamConfig.getConsumerGroupName(), + redisStreamConfig.getConsumerName() + ), + StreamOffset.create( + redisStreamConfig.getStreamKey(), + ReadOffset.lastConsumed() + ), + this + ); + + // redis stream 구독 생성까지 Blocking 된다. 이때의 timeout 2초다. 만약 2초보다 빠르게 구독이 생성되면 바로 다음으로 넘어간다. + this.subscription.await(Duration.ofSeconds(2)); + + // redis listen 시작 + this.listenerContainer.start(); + } + + @PreDestroy + public void destroy() { + if (this.subscription != null) { + this.subscription.cancel(); + } + if (this.listenerContainer != null) { + this.listenerContainer.stop(); + } + } +} diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/auction/controller/BuyerAuctionController.java b/src/main/java/com/wootecam/luckyvickyauction/core/auction/controller/BuyerAuctionController.java index 1c23d132..e16e1581 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/core/auction/controller/BuyerAuctionController.java +++ b/src/main/java/com/wootecam/luckyvickyauction/core/auction/controller/BuyerAuctionController.java @@ -10,8 +10,10 @@ import com.wootecam.luckyvickyauction.core.member.controller.Login; import com.wootecam.luckyvickyauction.core.member.dto.SignInInfo; import com.wootecam.luckyvickyauction.core.payment.service.PaymentService; +import com.wootecam.luckyvickyauction.global.dto.AuctionPurchaseRequestMessage; import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -53,7 +55,15 @@ public ResponseEntity submitAuction(@Login SignInInfo signInInfo, @CurrentTime LocalDateTime now, @PathVariable(name = "auctionId") Long auctionId, @RequestBody PurchaseRequest purchaseRequest) { - auctioneer.process(signInInfo, purchaseRequest.price(), auctionId, purchaseRequest.quantity(), now); + var message = new AuctionPurchaseRequestMessage( + UUID.randomUUID().toString(), + signInInfo.id(), + purchaseRequest.price(), + auctionId, + purchaseRequest.quantity(), + now + ); + auctioneer.process(message); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/Auctioneer.java b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/Auctioneer.java index c604efb4..cb154bdf 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/Auctioneer.java +++ b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/Auctioneer.java @@ -1,7 +1,7 @@ package com.wootecam.luckyvickyauction.core.auction.service; -import com.wootecam.luckyvickyauction.core.member.dto.SignInInfo; -import java.time.LocalDateTime; +import com.wootecam.luckyvickyauction.global.dto.AuctionPurchaseRequestMessage; +import com.wootecam.luckyvickyauction.global.dto.AuctionRefundRequestMessage; /** * 경매 입찰 로직 분리 @@ -9,6 +9,8 @@ */ public interface Auctioneer { - void process(SignInInfo buyerInfo, long price, long auctionId, long quantity, LocalDateTime requestTime); + void process(AuctionPurchaseRequestMessage message); + + void refund(AuctionRefundRequestMessage message); } diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java index 44ab1def..920f2230 100644 --- a/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java +++ b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/BasicAuctioneer.java @@ -4,22 +4,31 @@ import com.wootecam.luckyvickyauction.core.auction.service.AuctionService; import com.wootecam.luckyvickyauction.core.auction.service.Auctioneer; import com.wootecam.luckyvickyauction.core.member.dto.SignInInfo; +import com.wootecam.luckyvickyauction.core.member.domain.Member; +import com.wootecam.luckyvickyauction.core.member.domain.MemberRepository; import com.wootecam.luckyvickyauction.core.payment.domain.Receipt; import com.wootecam.luckyvickyauction.core.payment.domain.ReceiptRepository; import com.wootecam.luckyvickyauction.core.payment.domain.ReceiptStatus; import com.wootecam.luckyvickyauction.core.payment.service.PaymentService; import com.wootecam.luckyvickyauction.global.aop.DistributedLock; import java.time.LocalDateTime; +import com.wootecam.luckyvickyauction.global.dto.AuctionPurchaseRequestMessage; +import com.wootecam.luckyvickyauction.global.dto.AuctionRefundRequestMessage; +import com.wootecam.luckyvickyauction.global.exception.ErrorCode; +import com.wootecam.luckyvickyauction.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Primary @Service @RequiredArgsConstructor public class BasicAuctioneer implements Auctioneer { private final AuctionService auctionService; private final PaymentService paymentService; + private final MemberRepository memberRepository; private final ReceiptRepository receiptRepository; /** @@ -28,24 +37,35 @@ public class BasicAuctioneer implements Auctioneer { */ @Transactional @DistributedLock("#auctionId + ':auction:lock'") - public void process(SignInInfo buyerInfo, long price, long auctionId, long quantity, LocalDateTime requestTime) { - AuctionInfo auctionInfo = auctionService.getAuction(auctionId); - auctionService.submitPurchase(auctionId, price, quantity, requestTime); - - long buyerId = buyerInfo.id(); - long sellerId = auctionInfo.sellerId(); - paymentService.pointTransfer(buyerId, sellerId, price * quantity); + public void process(AuctionPurchaseRequestMessage message) { + Member buyer = findMemberObject(message.buyerId()); + AuctionInfo auctionInfo = auctionService.getAuction(message.auctionId()); + Member seller = findMemberObject(auctionInfo.sellerId()); + buyer.usePoint(message.price() * message.quantity()); + seller.chargePoint(message.price() * message.quantity()); + auctionService.submitPurchase(message.auctionId(), message.price(), message.quantity(), message.requestTime()); + Member savedBuyer = memberRepository.save(buyer); + Member savedSeller = memberRepository.save(seller); Receipt receipt = Receipt.builder() .productName(auctionInfo.productName()) - .price(price) - .quantity(quantity) + .price(message.price()) + .quantity(message.quantity()) .receiptStatus(ReceiptStatus.PURCHASED) - .sellerId(sellerId) - .buyerId(buyerId) - .auctionId(auctionId) + .sellerId(savedSeller.getId()) + .buyerId(savedBuyer.getId()) + .auctionId(message.auctionId()) .build(); receiptRepository.save(receipt); } + private Member findMemberObject(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NotFoundException("사용자를 찾을 수 없습니다. id=" + id, ErrorCode.M002)); + } + + @Override + public void refund(AuctionRefundRequestMessage message) { + + } } diff --git a/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/LazyAuctioneer.java b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/LazyAuctioneer.java new file mode 100644 index 00000000..15a52657 --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/core/auction/service/auctioneer/LazyAuctioneer.java @@ -0,0 +1,48 @@ +package com.wootecam.luckyvickyauction.core.auction.service.auctioneer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wootecam.luckyvickyauction.core.auction.service.Auctioneer; +import com.wootecam.luckyvickyauction.global.dto.AuctionPurchaseRequestMessage; +import com.wootecam.luckyvickyauction.global.dto.AuctionRefundRequestMessage; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.connection.stream.StringRecord; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LazyAuctioneer implements Auctioneer { + + private final StringRedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + @Value("${stream.key}") + private String streamKey; + + @Override + public void process(AuctionPurchaseRequestMessage message) { + + String messageType = "purchase"; + + try { + String stringMessage = objectMapper.writeValueAsString(message); + + StringRecord record = StreamRecords + .string(Map.of(messageType, stringMessage)) + .withStreamKey(streamKey); + redisTemplate.opsForStream().add(record); + + } catch (JsonProcessingException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Override + public void refund(AuctionRefundRequestMessage message) { + + } +} diff --git a/src/main/java/com/wootecam/luckyvickyauction/global/dto/AuctionPurchaseRequestMessage.java b/src/main/java/com/wootecam/luckyvickyauction/global/dto/AuctionPurchaseRequestMessage.java new file mode 100644 index 00000000..e7e0931a --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/global/dto/AuctionPurchaseRequestMessage.java @@ -0,0 +1,13 @@ +package com.wootecam.luckyvickyauction.global.dto; + +import java.time.LocalDateTime; + +public record AuctionPurchaseRequestMessage( + String requestId, + Long buyerId, + Long auctionId, + Long price, + Long quantity, + LocalDateTime requestTime +) { +} diff --git a/src/main/java/com/wootecam/luckyvickyauction/global/dto/AuctionRefundRequestMessage.java b/src/main/java/com/wootecam/luckyvickyauction/global/dto/AuctionRefundRequestMessage.java new file mode 100644 index 00000000..66fd6ba2 --- /dev/null +++ b/src/main/java/com/wootecam/luckyvickyauction/global/dto/AuctionRefundRequestMessage.java @@ -0,0 +1,4 @@ +package com.wootecam.luckyvickyauction.global.dto; + +public record AuctionRefundRequestMessage() { +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ca6b5d5c..f86c6aa6 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -22,7 +22,7 @@ spring: port: 6379 password: 1q2w3e host: localhost - + management: server: port: 8081 @@ -51,3 +51,8 @@ logging: org: hibernate: SQL: debug + +stream: + key: LVA-Stream + consumer: + groupName: LVA-Group diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 69bba3b2..f34e3382 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1,3257 +1,21 @@ - + - - - - -LuckyVickyAuction - - - - - + + + + + + + + + + Lucky Vicky + + - - -
-
-

사용자 가입 및 로그인(비로그인 가능)

-
-
-

회원가입

-
-

HTTP request

-
-
-
POST /auth/signup HTTP/1.1
-Content-Type: application/json
-Content-Type: application/json
-Content-Length: 82
-Host: localhost:8080
-
-{
-  "signUpId" : "userId",
-  "password" : "password1234",
-  "userRole" : "BUYER"
-}
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-

Request fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값설명제약사항

signUpId

String

true

회원가입을 진행할 아이디

password

String

true

회원가입을 진행할 패스워드

userRole

String

true

거래 권한 설정 (구매자 또는 판매자)

BUYER or SELLER

-
-
-
-

로그인

-
-

HTTP request

-
-
-
POST /auth/signin HTTP/1.1
-Content-Type: application/json
-Content-Type: application/json
-Content-Length: 54
-Host: localhost:8080
-
-{
-  "signInId" : "userId",
-  "password" : "password"
-}
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 23
-
-{
-  "role" : "SELLER"
-}
-
-
-
-
-

Request fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값설명제약사항

signInId

String

true

사용자가 입력한 아이디

password

String

true

사용자가 입력한 패스워드

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

role

String

true

사용자의 거래 권한 (구매자 또는 판매자)

-
-
-
-

로그아웃

-
-

HTTP request

-
-
-
POST /auth/signout HTTP/1.1
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-Content-Type: application/x-www-form-urlencoded
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-
-
-
-

포인트(구매자, 판매자 권한 필요)

-
-
-

포인트 충전

-
-

HTTP request

-
-
-
POST /payments/points/charge HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Accept: application/json
-Content-Length: 22
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-{
-  "amount" : 10000
-}
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Request fields

- ------- - - - - - - - - - - - - - - - - - - -
필드명타입필수값설명제약사항

amount

Number

true

충전할 포인트 금액

Long.MAX_VALUE까지 충전 가능

-
-
-
-
-
-

경매: 사용자(비로그인도 조회 가능)

-
-
-

경매 목록 조회

-
-

HTTP request

-
-
-
GET /auctions?offset=0&size=2 HTTP/1.1
-Content-Type: application/json
-Content-Type: application/json
-Host: localhost:8080
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 330
-
-[ {
-  "id" : 1,
-  "title" : "쓸만한 경매품 1",
-  "price" : 2000,
-  "startedAt" : "2024-08-23T11:11:18.105934",
-  "finishedAt" : "2024-08-23T11:41:18.105942"
-}, {
-  "id" : 2,
-  "title" : "쓸만한 경매품 2",
-  "price" : 4000,
-  "startedAt" : "2024-08-23T11:11:18.105961",
-  "finishedAt" : "2024-08-23T11:41:18.105962"
-} ]
-
-
-
-
-

Query parameters

- ---- - - - - - - - - - - - - - - - - -
ParameterDescription

offset

조회를 시작할 순서

size

조회할 페이지 크기

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

[].id

Number

true

경매 상품 ID

[].title

String

true

경매 상품 이름

[].price

Number

true

경매 상품 가격

[].startedAt

String

true

경매 시작 시간

[].finishedAt

String

true

경매 종료 시간

-
-
-
-

경매 상세 조회(고정 할인 정책 조회)

-
-

HTTP request

-
-
-
GET /auctions/1 HTTP/1.1
-Content-Type: application/json
-Content-Type: application/json
-Accept: application/json
-Host: localhost:8080
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 397
-
-{
-  "auctionId" : 1,
-  "sellerId" : 2,
-  "productName" : "테스트 상품",
-  "originPrice" : 10000,
-  "currentPrice" : 5000,
-  "originStock" : 100,
-  "currentStock" : 100,
-  "maximumPurchaseLimitCount" : 10,
-  "pricePolicy" : {
-    "type" : "CONSTANT",
-    "variationWidth" : 10
-  },
-  "variationDuration" : "PT1M",
-  "startedAt" : "2024-08-15T14:18:00",
-  "finishedAt" : "2024-08-15T15:18:00"
-}
-
-
-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /auctions/{auctionId}
ParameterDescription

auctionId

상세 조회 할 경매의 ID

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

auctionId

Number

true

경매 ID

sellerId

Number

true

판매자 ID

productName

String

true

상품 이름

originPrice

Number

true

상품 원가

currentPrice

Number

true

현재 가격

originStock

Number

true

재고를 보여주지 않는다면 NULL이고 응답 값에서 제외됨

원래 재고

currentStock

Number

true

재고를 보여주지 않는다면 NULL이고 응답 값에서 제외됨

현재 재고

maximumPurchaseLimitCount

Number

true

최대 구매 수량 제한

pricePolicy

Object

true

가격 정책

pricePolicy.type

String

true

가격 정책 타입

pricePolicy.variationWidth

Number

true

절대 변동 폭

variationDuration

String

true

가격 변동 주기

startedAt

String

true

경매 시작 시간

finishedAt

String

true

경매 종료 시간

-
-
-
-

경매 상세 조회(퍼센트 할인 정책 조회)

-
-

HTTP request

-
-
-
GET /auctions/1 HTTP/1.1
-Content-Type: application/json
-Content-Type: application/json
-Accept: application/json
-Host: localhost:8080
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 399
-
-{
-  "auctionId" : 1,
-  "sellerId" : 2,
-  "productName" : "테스트 상품",
-  "originPrice" : 10000,
-  "currentPrice" : 5000,
-  "originStock" : 100,
-  "currentStock" : 100,
-  "maximumPurchaseLimitCount" : 10,
-  "pricePolicy" : {
-    "type" : "PERCENTAGE",
-    "discountRate" : 10.0
-  },
-  "variationDuration" : "PT1M",
-  "startedAt" : "2024-08-15T14:18:00",
-  "finishedAt" : "2024-08-15T15:18:00"
-}
-
-
-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /auctions/{auctionId}
ParameterDescription

auctionId

상세 조회 할 경매의 ID

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

auctionId

Number

true

경매 ID

sellerId

Number

true

판매자 ID

productName

String

true

상품 이름

originPrice

Number

true

상품 원가

currentPrice

Number

true

현재 가격

originStock

Number

true

재고를 보여주지 않는다면 NULL이고 응답 값에서 제외됨

원래 재고

currentStock

Number

true

재고를 보여주지 않는다면 NULL이고 응답 값에서 제외됨

현재 재고

maximumPurchaseLimitCount

Number

true

최대 구매 수량 제한

pricePolicy

Object

true

가격 정책

pricePolicy.type

String

true

가격 정책 타입

pricePolicy.discountRate

Number

true

가격 변동 할인율

variationDuration

String

true

가격 변동 주기

startedAt

String

true

경매 시작 시간

finishedAt

String

true

경매 종료 시간

-
-
-
-
-
-

경매: 판매자(판매자 권한 필요)

-
-
-

경매 등록(고정 할인 정책)

-
-

HTTP request

-
-
-
POST /auctions HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Content-Length: 340
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-{
-  "productName" : "productName",
-  "originPrice" : 10000,
-  "stock" : 100,
-  "maximumPurchaseLimitCount" : 10,
-  "pricePolicy" : {
-    "type" : "CONSTANT",
-    "variationWidth" : 100
-  },
-  "variationDuration" : "PT10M",
-  "startedAt" : "2024-08-23T12:11:18.417372",
-  "finishedAt" : "2024-08-23T13:11:18.417372",
-  "isShowStock" : true
-}
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Request fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값설명제약사항

productName

String

true

상품 이름

originPrice

Number

true

상품 원가

stock

Number

true

상품 재고

maximumPurchaseLimitCount

Number

true

최대 구매 제한 수량

pricePolicy

Object

true

가격 정책

pricePolicy.type

String

true

가격 정책 타입

pricePolicy.variationWidth

Number

true

절대 가격 정책시 가격 절대 변동폭

variationDuration

String

true

경매 기간

isShowStock

Boolean

true

재고 노출 여부

startedAt

String

true

경매 시작 시간

finishedAt

String

true

경매 종료 시간

isShowStock

Boolean

true

재고 노출 여부

-
-
-
-

경매 등록(퍼센트 할인 정책)

-
-

HTTP request

-
-
-
POST /auctions HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Content-Length: 339
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-{
-  "productName" : "productName",
-  "originPrice" : 10000,
-  "stock" : 100,
-  "maximumPurchaseLimitCount" : 10,
-  "pricePolicy" : {
-    "type" : "PERCENTAGE",
-    "discountRate" : 10.0
-  },
-  "variationDuration" : "PT10M",
-  "startedAt" : "2024-08-23T12:11:18.39407",
-  "finishedAt" : "2024-08-23T13:11:18.39407",
-  "isShowStock" : true
-}
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Request fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값설명제약사항

productName

String

true

상품 이름

originPrice

Number

true

상품 원가

stock

Number

true

상품 재고

maximumPurchaseLimitCount

Number

true

최대 구매 제한 수량

pricePolicy

Object

true

가격 정책

pricePolicy.type

String

true

가격 정책 타입

pricePolicy.discountRate

Number

true

퍼센트 가격 정책시 가격 할인율

variationDuration

String

true

경매 기간

isShowStock

Boolean

true

재고 노출 여부

startedAt

String

true

경매 시작 시간

finishedAt

String

true

경매 종료 시간

isShowStock

Boolean

true

재고 노출 여부

-
-
-
-

경매 등록 취소

-
-

HTTP request

-
-
-
DELETE /auctions/1 HTTP/1.1
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /auctions/{auctionId}
ParameterDescription

auctionId

취소할 경매 ID

-
-
-
-

경매 목록 조회

-
-

HTTP request

-
-
-
GET /auctions/seller?offset=10&size=2 HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 502
-
-[ {
-  "id" : 1,
-  "title" : "내가 판매하는 경매품 1",
-  "originPrice" : 2000,
-  "currentPrice" : 1500,
-  "totalStock" : 100,
-  "currentStock" : 30,
-  "startedAt" : "2024-08-23T11:11:18.350541",
-  "finishedAt" : "2024-08-23T11:41:18.350548"
-}, {
-  "id" : 2,
-  "title" : "내가 판매하는 경매품 2",
-  "originPrice" : 4000,
-  "currentPrice" : 3500,
-  "totalStock" : 200,
-  "currentStock" : 60,
-  "startedAt" : "2024-08-23T11:11:18.350572",
-  "finishedAt" : "2024-08-23T11:41:18.350573"
-} ]
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Query parameters

- ---- - - - - - - - - - - - - - - - - -
ParameterDescription

offset

조회를 시작할 순서

size

조회할 페이지 크기

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

[].id

Number

true

경매 ID

[].title

String

true

경매 노출 제목

[].originPrice

Number

true

상품 원가

[].currentPrice

Number

true

현재 가격

[].totalStock

Number

true

총 재고

[].currentStock

Number

true

현재 남은 재고

[].startedAt

String

true

경매 시작 시간

[].finishedAt

String

true

경매 종료 시간

-
-
-
-

경매 상세 조회(고정 할인 정책 조회)

-
-

HTTP request

-
-
-
GET /auctions/1/seller HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 420
-
-{
-  "auctionId" : 1,
-  "productName" : "쓸만한 경매품",
-  "originPrice" : 10000,
-  "currentPrice" : 8000,
-  "originStock" : 100,
-  "currentStock" : 50,
-  "maximumPurchaseLimitCount" : 10,
-  "pricePolicy" : {
-    "type" : "CONSTANT",
-    "variationWidth" : 10
-  },
-  "variationDuration" : "PT10M",
-  "startedAt" : "2024-08-23T11:11:18.304446",
-  "finishedAt" : "2024-08-23T12:11:18.304452",
-  "isShowStock" : true
-}
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /auctions/{auctionId}/seller
ParameterDescription

auctionId

조회할 경매 ID

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

auctionId

Number

true

경매 ID

productName

String

true

상품 이름

originPrice

Number

true

상품 원가

currentPrice

Number

true

현재 가격

originStock

Number

true

원래 재고

currentStock

Number

true

현재 재고

maximumPurchaseLimitCount

Number

true

최대 구매 수량 제한

pricePolicy.type

String

true

가격 정책 유형

pricePolicy.variationWidth

Number

true

가격 변동 폭(고정 할인 정책)

variationDuration

String

true

가격 변동 주기(분)

startedAt

String

true

경매 시작 시간

finishedAt

String

true

경매 종료 시간

isShowStock

Boolean

true

재고 표시 여부

-
-
-
-

경매 상세 조회(퍼센트 할인 정책 조회)

-
-

HTTP request

-
-
-
GET /auctions/1/seller HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 422
-
-{
-  "auctionId" : 1,
-  "productName" : "쓸만한 경매품",
-  "originPrice" : 10000,
-  "currentPrice" : 8000,
-  "originStock" : 100,
-  "currentStock" : 50,
-  "maximumPurchaseLimitCount" : 10,
-  "pricePolicy" : {
-    "type" : "PERCENTAGE",
-    "discountRate" : 10.0
-  },
-  "variationDuration" : "PT10M",
-  "startedAt" : "2024-08-23T11:11:18.326445",
-  "finishedAt" : "2024-08-23T12:11:18.326449",
-  "isShowStock" : true
-}
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /auctions/{auctionId}/seller
ParameterDescription

auctionId

조회할 경매 ID

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

auctionId

Number

true

경매 ID

productName

String

true

상품 이름

originPrice

Number

true

상품 원가

currentPrice

Number

true

현재 가격

originStock

Number

true

원래 재고

currentStock

Number

true

현재 재고

maximumPurchaseLimitCount

Number

true

최대 구매 수량 제한

pricePolicy.type

String

true

가격 정책 유형

pricePolicy.discountRate

Number

true

가격 변동 폭(퍼센트 할인 정책)

variationDuration

String

true

가격 변동 주기(분)

startedAt

String

true

경매 시작 시간

finishedAt

String

true

경매 종료 시간

isShowStock

Boolean

true

재고 표시 여부

-
-
-
-
-
-

경매: 구매자(구매자 권한 필요)

-
-
-

경매 입찰

-
-

HTTP request

-
-
-
POST /auctions/1/purchase HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Content-Length: 40
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-{
-  "price" : 10000,
-  "quantity" : 20
-}
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /auctions/{auctionId}/purchase
ParameterDescription

auctionId

입찰한 경매의 ID

-
-
-

Request fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값설명제약사항

price

Number

true

입찰을 희망하는 가격

quantity

Number

true

입찰을 희망하는 수량

-
-
-
-

경매 환불

-
-

HTTP request

-
-
-
PUT /receipts/1/refund HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Accept: application/json
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /receipts/{receiptId}/refund
ParameterDescription

receiptId

환불할 경매의 영수증 ID

-
-
-
-
-
-

거래 이력

-
-
-

구매자 거래 목록 조회(구매자 권한 필요)

-
-

HTTP request

-
-
-
GET /receipts/buyer?offset=3&size=10 HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 425
-
-[ {
-  "id" : 1,
-  "auctionId" : 1,
-  "type" : "PURCHASED",
-  "productName" : "내가 구매한 상품1",
-  "quantity" : 10,
-  "price" : 1000
-}, {
-  "id" : 2,
-  "auctionId" : 2,
-  "type" : "REFUND",
-  "productName" : "내가 구매한 상품2",
-  "quantity" : 20,
-  "price" : 2000
-}, {
-  "id" : 3,
-  "auctionId" : 3,
-  "type" : "PURCHASED",
-  "productName" : "내가 구매한 상품3",
-  "quantity" : 30,
-  "price" : 3000
-} ]
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Query parameters

- ---- - - - - - - - - - - - - - - - - -
ParameterDescription

offset

조회를 시작할 순서

size

조회할 페이지 크기

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

[].id

Number

true

거래 내역 ID

[].auctionId

Number

true

구매한 경매의 ID

[].type

String

true

거래 타입 (구매완료, 환불완료)

[].productName

String

true

상품명

[].quantity

Number

true

구매 수량

[].price

Number

true

구매 가격

-
-
-
-

판매자 거래 목록 조회(판매자 권한 필요)

-
-

HTTP request

-
-
-
GET /receipts/seller?offset=3&size=10 HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 422
-
-[ {
-  "id" : 1,
-  "auctionId" : 1,
-  "type" : "REFUND",
-  "productName" : "내가 판매한 상품1",
-  "price" : 1000,
-  "quantity" : 10
-}, {
-  "id" : 2,
-  "auctionId" : 2,
-  "type" : "PURCHASED",
-  "productName" : "내가 판매한 상품2",
-  "price" : 2000,
-  "quantity" : 20
-}, {
-  "id" : 3,
-  "auctionId" : 3,
-  "type" : "REFUND",
-  "productName" : "내가 판매한 상품3",
-  "price" : 3000,
-  "quantity" : 30
-} ]
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Query parameters

- ---- - - - - - - - - - - - - - - - - -
ParameterDescription

offset

조회를 시작할 순서

size

조회할 페이지 크기

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

[].id

Number

true

거래 내역 ID

[].auctionId

Number

true

구매한 경매의 ID

[].type

String

true

거래 타입 (구매완료, 환불완료)

[].productName

String

true

상품명

[].quantity

Number

true

구매 수량

[].price

Number

true

구매 가격

-
-
-
-

거래 상세 조회(판매자, 구매자 권한 필요)

-
-

HTTP request

-
-
-
GET /receipts/1 HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-Cookie: JSESSIONID=sessionId
-
-
-
-
-

HTTP response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json
-Content-Length: 267
-
-{
-  "receiptId" : 1,
-  "productName" : "상품명",
-  "price" : 1000,
-  "quantity" : 1,
-  "receiptStatus" : "PURCHASED",
-  "auctionId" : 1,
-  "sellerId" : 1,
-  "buyerId" : 2,
-  "createdAt" : "2024-08-23T11:11:18.234072",
-  "updatedAt" : "2024-08-23T12:11:18.234078"
-}
-
-
-
-
-

Request cookies

- ---- - - - - - - - - - - - - -
NameDescription

JSESSIONID

세션 ID

-
-
-

Path parameters

- - ---- - - - - - - - - - - - - -
Table 1. /receipts/{receiptId}
ParameterDescription

receiptId

조회할 거래내역 ID

-
-
-

Response fields

- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값양식설명

receiptId

Number

true

거래 내역 ID

productName

String

true

상품명

price

Number

true

구매 가격

quantity

Number

true

구매 수량

receiptStatus

String

true

입찰 상태 (구매완료, 환불완료)

auctionId

Number

true

경매 ID

sellerId

Number

true

판매자 ID

buyerId

Number

true

구매자 ID

createdAt

String

true

경매 입찰 시간

updatedAt

String

true

경매 마지막 수정 시간

-
-
-
-
-
-

에러 코드

-
-
-

Error code

- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
코드코드명

A000

경매 재고는 인당 구매 수량보다 작을 수 없습니다.

A001

상품 이름은 비어있을 수 없습니다.

A002

상품 원가는 0과 같거나 작을 수 없습니다.

A003

최대 구매 수량 제한은 0과 같거나 작을 수 없습니다.

A004

가격 변동폭은 0과 같거나 작을 수 없습니다.

A005

변동 시간 단위는 0과 같거나 작을 수 없습니다.

A006

경매의 시작 시간은 종료 시간보다 클 수 없습니다.

A007

경매 생성 시, 경매 지속시간은 10, 20, 30, 40, 50, 60분이 아닌 경우 예외가 발생합니다.

A008

경매 가격 변동폭은 경매 가격보다 낮아야 합니다.

A009

경매 생성 시, 할인율이 0퍼센트 미만이거나 50 퍼센트를 초과한 경우 예외가 발생합니다.

A010

경매ID를 기준으로 경매를 찾으려고 했지만 찾을 수 없습니다.

A011

경매 정보 생성 시, 현재 가격은 0과 같거나 작을 경우 예외가 발생합니다.

A012

요청 수량만큼 경매 상품 구입이 불가능 할 때 예외가 발생합니다.

A013

진행 중이지 않은 경매를 입찰하려고 할 때, 예외가 발생합니다.

A014

경매 생성 시, 경매 시작 시간이 현재 시간보다 이른 경우 예외가 발생합니다.

A015

환불로 인한 재고 변경 시, 추가 및 삭제하는 재고량이 1 미만일 경우 예외가 발생합니다.

A016

환불로 인한 재고 변경 시, 변경 후의 재고가 원래 재고보다 많을 경우 예외가 발생합니다.

A017

경매 취소 시, 사용자 역할이 판매자가 아닌 경우 예외가 발생합니다.

A018

경매 취소 시, 경매를 생성한 판매자와 경매 취소 요청 사용자가 다를 경우 예외가 발생합니다.

A019

경매 취소 시, 경매가 준비중인 상태가 아닐때 예외가 발생합니다.

A020

경매 상세 조회 시, 요청한 판매자가 경매의 판매자가 아닐 경우 예외가 발생합니다.

A021

경매 생성 시, 가격 변동 정책이 적용된 경매에서 최대 할인가를 적용했을떄 최소 가격 이하로 떨어지는 경우 예외가 발생합니다.

A022

경매 입찰 진행 시, 현재 상품의 가격과 사용자가 구매 요청한 가격이 다를때 예외가 발생합니다.

A023

가격 정책 객체를 JSON으로 변환 시, 변환이 불가능할 경우 예외가 발생합니다.

A024

JSON을 가격 정책 객체로 변환 시, 타입이 올바르지 않을 경우 예외가 발생합니다.

A025

JSON을 가격 정책 객체로 변환 시, 변환이 불가능할 경우 예외가 발생합니다.

A026

경매 입찰 요청 시, 요청 가격이 0보다 작으면 예외가 발생합니다.

A027

경매 입찰 요청 시, 요청 수량이 1 미만일 경우 예외가 발생합니다.

A028

경매 생성 시, 경매 할인 주기 시간이 1, 5, 10분이 아닌 경우 예외가 발생합니다.

A029

경매 생성 시, 경매 할인 주기 시간이 경매 지속 시간보다 크거나 같은 경우 예외가 발생합니다.

R000

거래 내역 조회 시, 거래 내역을 찾을 수 없을 경우 예외가 발생합니다.

R001

거래 내역 조회 시, 사용자가 해당 거래의 구매자 또는 판매자가 아닌 경우 예외가 발생합니다.

R002

환불 시, 이미 환불된 거래 내역일 경우 예외가 발생합니다.

M000

로그인(회원가입) 시, 이미 존재하는 회원 아이디로 로그인을 시도한 경우 예외가 발생합니다.

M001

로그인(회원가입) 시, 사용자의 역할(구매자, 판매자)를 찾을 수 없는 경우 예외가 발생합니다.

M002

사용자 조회 시, 사용자를 찾을 수 없는 경우 예외가 발생합니다.

M003

로그인 시, 입력 패스워드와 실제 패스워드가 다른 경우 예외가 발생합니다.

M004

회원가입 시, 사용자 아이디가 비어있는 경우 예외가 발생합니다.

M005

회원가입 시, 사용자 아이디가 글자수 제한 정책에 맞지 않으면 예외가 발생합니다.

M006

회원가입 시, 비밀번호가 빈칸 또는 공백인 경우 예외가 발생합니다.

M007

회원가입 시, 비밀번호는 8자 이상 20자 이하가 아닌 경우 예외가 발생합니다.

M008

회원가입 시, 비밀번호에 숫자가 포함되어 있지 않은 경우 예외가 발생합니다.

M009

회원가입 시, 비밀번호에 알파벳 소문자가 포함되어 있지 않은 경우 예외가 발생합니다.

M010

회원가입 시, 비밀번호에 영문자와 숫자 외에 다른 문자가 포함되어 있는 경우 예외가 발생합니다.

P000

구매 시, 로그인한 사용자가 구매자가 아닌 경우 예외가 발생합니다.

P001

구매 시, 사용자의 포인트가 부족한 경우 예외가 발생합니다.

P002

거래 내역 조회 시, 거래 내역을 찾을 수 없을 경우 예외가 발생합니다.

P004

환불 시, 요청한 사용자가 환불할 거래의 구매자가 아닌 경우 예외가 발생합니다.

P005

포인트 충전 시, 충전할 포인트가 0보다 작을 경우 예외가 발생합니다.

P006

포인트 충전 시, 충전 후 포인트가 Long 최대값을 초과할 경우 예외가 발생합니다.

P007

환불 요청 시, 종료된 경매에 대한 환불이 아닌 경우 예외가 발생합니다.

AU00

API 요청 시, 비로그인 사용자가 허락되지 않은 엔드포인트에 접근 할 경우 예외가 발생합니다.

AU01

API 요청 시, 판매자가 아닌 사람이 판매자만 접근할 수 있는 엔드포인트에 접근 할 경우 예외가 발생합니다.

AU02

API 요청 시, 구매자가 아닌 사람이 구매자만 접근할 수 있는 엔드포인트에 접근 할 경우 예외가 발생합니다.

AU03

API 요청 시, Roles에 명시된 권한을 하나라도 갖지 않은 사용자가 엔드포인트에 접근 할 경우 예외가 발생합니다.

G000

DTO 생성 시, 필드의 값이 NULL인 경우 예외가 발생합니다.

G001

목록 조회시, 과도한 데이터를 조회할 수 없습니다.

SERVER_ERROR

서버에서 예기치 못한 예외가 발생한 경우

-
-
-
-
- - - + + +
\ No newline at end of file diff --git a/src/test/java/com/wootecam/luckyvickyauction/context/RepositoryTest.java b/src/test/java/com/wootecam/luckyvickyauction/context/RepositoryTest.java index 77a61bac..3149892f 100644 --- a/src/test/java/com/wootecam/luckyvickyauction/context/RepositoryTest.java +++ b/src/test/java/com/wootecam/luckyvickyauction/context/RepositoryTest.java @@ -9,10 +9,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; // 중간에서 맵핑을 담당하는 CoreRepository 의 경우 자동 생성 대상이 아니므로 직접 명시해 줄 것 @Import({JpaConfig.class, MemberCoreRepository.class, AuctionCoreRepository.class, ReceiptCoreRepository.class}) @DataJpaTest +@ActiveProfiles("test") public abstract class RepositoryTest { @Autowired diff --git a/src/test/java/com/wootecam/luckyvickyauction/context/ServiceTest.java b/src/test/java/com/wootecam/luckyvickyauction/context/ServiceTest.java index dff7375e..a6bcfdb8 100644 --- a/src/test/java/com/wootecam/luckyvickyauction/context/ServiceTest.java +++ b/src/test/java/com/wootecam/luckyvickyauction/context/ServiceTest.java @@ -14,7 +14,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") @SpringBootTest(webEnvironment = WebEnvironment.NONE) public abstract class ServiceTest { diff --git a/src/test/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentServiceTest.java b/src/test/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentServiceTest.java index c2a112de..16150c6d 100644 --- a/src/test/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentServiceTest.java +++ b/src/test/java/com/wootecam/luckyvickyauction/core/payment/service/PaymentServiceTest.java @@ -15,6 +15,7 @@ import com.wootecam.luckyvickyauction.core.member.fixture.MemberFixture; import com.wootecam.luckyvickyauction.core.payment.domain.Receipt; import com.wootecam.luckyvickyauction.core.payment.domain.ReceiptStatus; +import com.wootecam.luckyvickyauction.global.dto.AuctionPurchaseRequestMessage; import com.wootecam.luckyvickyauction.global.exception.AuthorizationException; import com.wootecam.luckyvickyauction.global.exception.BadRequestException; import com.wootecam.luckyvickyauction.global.exception.ErrorCode; @@ -65,8 +66,15 @@ class 정상적인_요청_흐름이면 { Auction savedAuction = auctionRepository.save(runningAuction); // when - auctioneer.process(new SignInInfo(savedBuyer.getId(), Role.BUYER), 10000L, savedAuction.getId(), 1L, - now.minusMinutes(30)); + var message = new AuctionPurchaseRequestMessage( + "test", + savedBuyer.getId(), + savedAuction.getId(), + 10000L, + 1L, + now.minusMinutes(30) + ); + auctioneer.process(message); // then Auction auction = auctionRepository.findById(savedAuction.getId()).get(); @@ -118,8 +126,17 @@ class 만약_요청한_구매자를_찾을_수_없다면 { // expect assertThatThrownBy( - () -> auctioneer.process(new SignInInfo(savedBuyer.getId() + 1L, Role.BUYER), 10000L, - savedAuction.getId(), 1L, now.minusMinutes(30))) + () -> { + var message = new AuctionPurchaseRequestMessage( + "test", + savedBuyer.getId() + 1L, + savedAuction.getId(), + 10000L, + 1L, + now.minusMinutes(30) + ); + auctioneer.process(message); + }) .isInstanceOf(NotFoundException.class) .hasMessage("사용자를 찾을 수 없습니다. id=" + (savedBuyer.getId() + 1L)) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.M002); @@ -164,8 +181,17 @@ class 만약_판매자를_찾을_수_없다면 { // expect assertThatThrownBy( - () -> auctioneer.process(new SignInInfo(savedBuyer.getId(), Role.BUYER), 10000L, - savedAuction.getId(), 1L, now.minusMinutes(30))) + () -> { + var message = new AuctionPurchaseRequestMessage( + "test", + savedBuyer.getId(), + savedAuction.getId(), + 10000L, + 1L, + now.minusMinutes(30) + ); + auctioneer.process(message); + }) .isInstanceOf(NotFoundException.class) .hasMessage("사용자를 찾을 수 없습니다. id=" + runningAuction.getSellerId()) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.M002); @@ -173,7 +199,7 @@ class 만약_판매자를_찾을_수_없다면 { } @Nested - class 만약_요청한_물건의_금액이_사용자가_가진_포인트보다_크다면 { + class 만약_요청한_물건의_금액이_사용자가_요청한_금액과_다르다면 { @Test void 예외가_발생한다() { @@ -210,8 +236,17 @@ class 만약_요청한_물건의_금액이_사용자가_가진_포인트보다_ // expect assertThatThrownBy( - () -> auctioneer.process(new SignInInfo(savedBuyer.getId(), Role.BUYER), 10000L, - savedAuction.getId(), 1L, now.minusMinutes(30))) + () -> { + var message = new AuctionPurchaseRequestMessage( + "test", + savedBuyer.getId(), + savedAuction.getId(), + 10000L, + 1L, + now.minusMinutes(30) + ); + auctioneer.process(message); + }) .isInstanceOf(BadRequestException.class) .hasMessage(String.format("입력한 가격으로 상품을 구매할 수 없습니다. 현재가격: %d 입력가격: %d", savedAuction.getCurrentPrice(), 10000L)) diff --git a/src/test/java/com/wootecam/luckyvickyauction/documentation/BuyerAuctionDocument.java b/src/test/java/com/wootecam/luckyvickyauction/documentation/BuyerAuctionDocument.java index 8b951ba2..9b37d948 100644 --- a/src/test/java/com/wootecam/luckyvickyauction/documentation/BuyerAuctionDocument.java +++ b/src/test/java/com/wootecam/luckyvickyauction/documentation/BuyerAuctionDocument.java @@ -2,7 +2,6 @@ import static com.wootecam.luckyvickyauction.documentation.DocumentFormatGenerator.getAttribute; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; @@ -187,7 +186,7 @@ class 구매자_경매_입찰 { PurchaseRequest purchaseRequest = new PurchaseRequest(10000L, 20L); SignInInfo buyerInfo = new SignInInfo(1L, Role.BUYER); willDoNothing().given(auctioneer) - .process(any(SignInInfo.class), anyLong(), anyLong(), anyLong(), any(LocalDateTime.class)); + .process(any()); given(authenticationContext.getPrincipal()).willReturn(buyerInfo); mockMvc.perform(post("/auctions/{auctionId}/purchase", auctionId) diff --git a/src/test/resources/application.yaml b/src/test/resources/application-test.yaml similarity index 100% rename from src/test/resources/application.yaml rename to src/test/resources/application-test.yaml