
https://school.programmers.co.kr/learn/courses/30/lessons/142086
— 문제 설명
문자열 s가 주어졌을 때, s의 각 위치마다 자신보다 앞에 나왔으면서, 자신과 가장 가까운 곳에 있는 같은 글자가 어디 있는지 알고 싶습니다.
예를 들어, s="banana"라고 할 때, 각 글자들을 왼쪽부터 오른쪽으로 읽어 나가면서 다음과 같이 진행할 수 있습니다.
따라서 최종 결과물은 [-1, -1, -1, 2, 2, 2]가 됩니다.
문자열 s이 주어질 때, 위와 같이 정의된 연산을 수행하는 함수 solution을 완성해주세요.
— 제한 조건
s의 길이 ≤ 10,000s은 영어 소문자로만 이루어져 있습니다.— 입출력 예
| s | result |
|---|---|
| "banana" | [-1, -1, -1, 2, 2, 2] |
| "foobar" | [-1, -1, 1, -1, -1, -1] |
입출력 예 #1
지문과 같습니다.
입출력 예 #2
설명 생략
— 문제 풀이
class Solution {
public int[] solution(String s) {
int[] answer = new int[s.length()];
int[] alpha = new int[26]; // 알파벳 위치
for(int i=0;i<26;i++){
alpha[i] = -1;
}
for(int i=0;i<s.length();i++){
int cur = (int)s.charAt(i)-'a'; // 현재 알파벳
if(alpha[cur]==-1){ // 처음 나온 알파벳
answer[i] = -1;
alpha[cur] = i;
}else {
answer[i] = i - alpha[cur];
alpha[cur] = i;
}
}
return answer;
}
}
https://school.programmers.co.kr/learn/courses/30/lessons/134240
— 문제 설명
수웅이는 매달 주어진 음식을 빨리 먹는 푸드 파이트 대회를 개최합니다. 이 대회에서 선수들은 1대 1로 대결하며, 매 대결마다 음식의 종류와 양이 바뀝니다. 대결은 준비된 음식들을 일렬로 배치한 뒤, 한 선수는 제일 왼쪽에 있는 음식부터 오른쪽으로, 다른 선수는 제일 오른쪽에 있는 음식부터 왼쪽으로 순서대로 먹는 방식으로 진행됩니다. 중앙에는 물을 배치하고, 물을 먼저 먹는 선수가 승리하게 됩니다.
이때, 대회의 공정성을 위해 두 선수가 먹는 음식의 종류와 양이 같아야 하며, 음식을 먹는 순서도 같아야 합니다. 또한, 이번 대회부터는 칼로리가 낮은 음식을 먼저 먹을 수 있게 배치하여 선수들이 음식을 더 잘 먹을 수 있게 하려고 합니다. 이번 대회를 위해 수웅이는 음식을 주문했는데, 대회의 조건을 고려하지 않고 음식을 주문하여 몇 개의 음식은 대회에 사용하지 못하게 되었습니다.
예를 들어, 3가지의 음식이 준비되어 있으며, 칼로리가 적은 순서대로 1번 음식을 3개, 2번 음식을 4개, 3번 음식을 6개 준비했으며, 물을 편의상 0번 음식이라고 칭한다면, 두 선수는 1번 음식 1개, 2번 음식 2개, 3번 음식 3개씩을 먹게 되므로 음식의 배치는 "1223330333221"이 됩니다. 따라서 1번 음식 1개는 대회에 사용하지 못합니다.
수웅이가 준비한 음식의 양을 칼로리가 적은 순서대로 나타내는 정수 배열 food가 주어졌을 때, 대회를 위한 음식의 배치를 나타내는 문자열을 return 하는 solution 함수를 완성해주세요.
— 제한 조건
food의 길이 ≤ 9food의 각 원소 ≤ 1,000food에는 칼로리가 적은 순서대로 음식의 양이 담겨 있습니다.food[i]는 i번 음식의 수입니다.food[0]은 수웅이가 준비한 물의 양이며, 항상 1입니다.— 입출력 예
| food | result |
|---|---|
| [1, 3, 4, 6] | "1223330333221" |
| [1, 7, 1, 2] | "111303111" |
입출력 예 #1
입출력 예 #2
— 문제 풀이
class Solution {
public String solution(int[] food) {
StringBuilder sb = new StringBuilder();
int [] count = new int[food.length];
count[0] = 1;
int length = 0;
for(int i=1;i<food.length;i++){
count[i] = food[i]/2;
length += food[i]/2;
}
// 왼쪽 선수
for(int i=1;i<food.length;i++){
int cur = i;
for(int j=0;j<count[cur];j++){
sb.append(cur + "");
}
}
sb.append("0"); // 물
// 오른쪽 선수
for(int i=food.length-1;i>0;i--){
int cur = i;
for(int j=0;j<count[cur];j++){
sb.append(cur + "");
}
}
return sb.toString();
}
}
https://school.programmers.co.kr/learn/courses/30/lessons/132267
— 문제 설명
오래전 유행했던 콜라 문제가 있습니다. 콜라 문제의 지문은 다음과 같습니다.
정답은 아무에게도 말하지 마세요.
콜라 빈 병 2개를 가져다주면 콜라 1병을 주는 마트가 있다. 빈 병 20개를 가져다주면 몇 병을 받을 수 있는가?
단, 보유 중인 빈 병이 2개 미만이면, 콜라를 받을 수 없다.
문제를 풀던 상빈이는 콜라 문제의 완벽한 해답을 찾았습니다. 상빈이가 푼 방법은 아래 그림과 같습니다. 우선 콜라 빈 병 20병을 가져가서 10병을 받습니다. 받은 10병을 모두 마신 뒤, 가져가서 5병을 받습니다. 5병 중 4병을 모두 마신 뒤 가져가서 2병을 받고, 또 2병을 모두 마신 뒤 가져가서 1병을 받습니다. 받은 1병과 5병을 받았을 때 남은 1병을 모두 마신 뒤 가져가면 1병을 또 받을 수 있습니다. 이 경우 상빈이는 총 10 + 5 + 2 + 1 + 1 = 19병의 콜라를 받을 수 있습니다.
문제를 열심히 풀던 상빈이는 일반화된 콜라 문제를 생각했습니다. 이 문제는 빈 병 a개를 가져다주면 콜라 b병을 주는 마트가 있을 때, 빈 병 n개를 가져다주면 몇 병을 받을 수 있는지 계산하는 문제입니다. 기존 콜라 문제와 마찬가지로, 보유 중인 빈 병이 a개 미만이면, 추가적으로 빈 병을 받을 순 없습니다. 상빈이는 열심히 고심했지만, 일반화된 콜라 문제의 답을 찾을 수 없었습니다. 상빈이를 도와, 일반화된 콜라 문제를 해결하는 프로그램을 만들어 주세요.
콜라를 받기 위해 마트에 주어야 하는 병 수 a, 빈 병 a개를 가져다 주면 마트가 주는 콜라 병 수 b, 상빈이가 가지고 있는 빈 병의 개수 n이 매개변수로 주어집니다. 상빈이가 받을 수 있는 콜라의 병 수를 return 하도록 solution 함수를 작성해주세요.
— 제한 조건
b < a ≤ n ≤ 1,000,000— 입출력 예
| a | b | n | result |
|---|---|---|---|
| 2 | 1 | 20 | 19 |
| 3 | 1 | 20 | 9 |
입출력 예 #1
입출력 예 #2
— 문제 풀이
class Solution {
public int count = 0;
public int solution(int a, int b, int n) {
cal(a,b,n);
return count;
}
public void cal(int a, int b, int n){ // a 빈 병, b 돌려주는 콜라 수, n 가진 병 갯수
if(n<a) return;
else {
int received = (n / a) * b; // 받는 병 갯수
count += received;
cal(a,b, (n % a) + received);
}
}
}
https://school.programmers.co.kr/learn/courses/30/lessons/138477
— 문제 설명
"명예의 전당"이라는 TV 프로그램에서는 매일 1명의 가수가 노래를 부르고, 시청자들의 문자 투표수로 가수에게 점수를 부여합니다. 매일 출연한 가수의 점수가 지금까지 출연 가수들의 점수 중 상위 k번째 이내이면 해당 가수의 점수를 명예의 전당이라는 목록에 올려 기념합니다. 즉 프로그램 시작 이후 초기에 k일까지는 모든 출연 가수의 점수가 명예의 전당에 오르게 됩니다. k일 다음부터는 출연 가수의 점수가 기존의 명예의 전당 목록의 k번째 순위의 가수 점수보다 더 높으면, 출연 가수의 점수가 명예의 전당에 오르게 되고 기존의 k번째 순위의 점수는 명예의 전당에서 내려오게 됩니다.
이 프로그램에서는 매일 "명예의 전당"의 최하위 점수를 발표합니다. 예를 들어, k = 3이고, 7일 동안 진행된 가수의 점수가 [10, 100, 20, 150, 1, 100, 200]이라면, 명예의 전당에서 발표된 점수는 아래의 그림과 같이 [10, 10, 10, 20, 20, 100, 100]입니다.
명예의 전당 목록의 점수의 개수 k, 1일부터 마지막 날까지 출연한 가수들의 점수인 score가 주어졌을 때, 매일 발표된 명예의 전당의 최하위 점수를 return하는 solution 함수를 완성해주세요.
— 제한 조건
k ≤ 100score의 길이 ≤ 1,000score[i] ≤ 2,000— 입출력 예
| k | score | result |
|---|---|---|
| 3 | [10, 100, 20, 150, 1, 100, 200] | [10, 10, 10, 20, 20, 100, 100] |
| 4 | [0, 300, 40, 300, 20, 70, 150, 50, 500, 1000] | [0, 0, 0, 0, 20, 40, 70, 70, 150, 300] |
입출력 예 #1
입출력 예 #2
— 문제 풀이
import java.util.*;
class Solution {
public int[] solution(int k, int[] score) {
Queue<Integer> pq = new PriorityQueue<>();
int[] answer = new int[score.length];
for(int i=0;i<score.length;i++){
if(pq.size()<k){
pq.add(score[i]);
int lowest = pq.poll();
answer[i] = lowest;
pq.add(lowest);
}else {
int lowest = pq.poll();
if(score[i]<=lowest){
answer[i] = lowest;
pq.add(lowest);
}else {
pq.add(score[i]);
int nLowest = pq.poll();
answer[i] = nLowest;
pq.add(nLowest);
}
}
}
return answer;
}
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}spring:
application:
name: eureka-server
server:
port: 19090
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:19090/eureka/
instance:
hostname: localhost
@SpringBootApplication
@EnableEurekaServer
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}runtimeOnly 'org.hsqldb:hsqldb'dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.hsqldb:hsqldb'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}@SpringBootApplication
@EnableFeignClients
@EnableJpaAuditing
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
////////////////////////////////////////////////
@SpringBootApplication
@EnableFeignClients
@EnableJpaAuditing
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}spring:
application:
name: order-service # product는 product-service
server:
port: 19092 # product는 19093
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductReqDto {
private String name;
private String description;
private Integer price;
private Integer quantity;
public Product toEntity(){
return Product.builder()
.name(this.name)
.description(this.description)
.price(this.price)
.quantity(this.quantity).build();
}
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProductResDto {
private Long id;
private String name;
private String description;
private Integer price;
private Integer quantity;
}@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private Integer price;
private Integer quantity;
@CreatedDate
private LocalDateTime createdAt;
private String createdBy;
@LastModifiedDate
private LocalDateTime updatedAt;
private String updatedBy;
private LocalDateTime deletedAt;
private String deletedBy;
public void setCreated(String userId){
this.createdBy = userId;
}
public void setUpdated(String userId){
this.updatedBy = userId;
}
public void setDeleted(String userId){
this.deletedBy = userId;
this.deletedAt = LocalDateTime.now();
}
public void updateProduct(String name, String description, Integer price, Integer quantity){
this.name = name;
this.description = description;
this.price = price;
this.quantity = quantity;
}
public ProductResDto toResDto(){
return new ProductResDto(
this.id,
this.name,
this.description,
this.price,
this.quantity
);
}
}public interface ProductRepository extends JpaRepository<Product, Long> {
}@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProductService {
private final ProductRepository productRepository;
@Transactional
public ProductResDto createProduct(ProductReqDto requestDto, String userId) {
Product product = requestDto.toEntity();
product.setCreated(userId);
product.setUpdated(userId);
return productRepository.save(product).toResDto();
}
@Transactional
public ProductResDto updateProduct(Long productId,ProductReqDto requestDto, String userId) {
Product product = productRepository.findById(productId)
.filter(p -> p.getDeletedAt() == null)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found or has been deleted"));
product.updateProduct(requestDto.getName(), requestDto.getDescription(), requestDto.getPrice(), requestDto.getQuantity());
product.setUpdated(userId);
return product.toResDto();
}
@Transactional
public void deleteProduct(Long productId, String userId) {
Product product = productRepository.findById(productId)
.filter(p -> p.getDeletedAt() == null)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found or has been deleted"));
product.setDeleted(userId);
}
public ProductResDto getProductById(Long productId){
Product product = productRepository.findById(productId)
.filter(p -> p.getDeletedAt() == null)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found or has been deleted"));
return product.toResDto();
}
@Transactional
public void reduceQuantity(Long productId, Integer quantity, String userId) {
Product product = productRepository.findById(productId)
.filter(p -> p.getDeletedAt() == null)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found or has been deleted"));
if(quantity > product.getQuantity()){
throw new IllegalArgumentException("Not Enough Product");
}
product.updateProduct(product.getName(), product.getDescription(), product.getPrice(), product.getQuantity() - quantity);
product.setUpdated(userId);
}
}
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
/*
상품 등록
path /api/products
Request
header X-User-Id : userId
body ProductReqDto
Response
ProductResDto
*/
@PostMapping
public ResponseEntity<ProductResDto> createProduct(@RequestBody ProductReqDto reqDto,
@RequestHeader(value = "X-User-Id") String userId) {
ProductResDto res = productService.createProduct(reqDto, userId);
return ResponseEntity.ok(res);
}
/*
상품 단건 조회
Request
Path /api/products/{productId}
Response
ProductResDto
*/
@GetMapping("/{productId}")
public ResponseEntity<ProductResDto> getProductById(@PathVariable Long productId) {
ProductResDto res = productService.getProductById(productId);
return ResponseEntity.ok(res);
}
/*
상품 정보 업데이트
path /api/products/{productId}
Request
header X-User-Id : userId
body ProductReqDto
Response
ProductResDto
*/
@PutMapping("/{productId}")
public ResponseEntity<ProductResDto> updateProduct(@PathVariable Long productId,
@RequestBody ProductReqDto reqDto, @RequestHeader(value = "X-User-Id") String userId) {
ProductResDto res = productService.updateProduct(productId, reqDto, userId);
return ResponseEntity.ok(res);
}
/*
상품 삭제
path /api/products/{productId}
Request
header X-User-Id : userId
Response
message
*/
@DeleteMapping("/{productId}")
public ResponseEntity<String> deleteProduct(@PathVariable Long productId,
@RequestHeader(value = "X-User-Id") String userId) {
productService.deleteProduct(productId, userId);
return ResponseEntity.ok("Deleted product successfully");
}
/*
상품 삭제
path /api/products/{productId}/reduceQuantity
Request
header X-User-Id : userId
param quantity
Response
message
*/
@PutMapping("/{productId}/reduceQuantity")
public ResponseEntity<String> reduceQuantity(@PathVariable Long productId,
@RequestParam Integer quantity,
@RequestHeader(value = "X-User-Id") String userId) {
productService.reduceQuantity(productId, quantity, userId);
return ResponseEntity.ok("Reduced quantity of product successfully");
}
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class OrderReqDto {
private List<Long> orderItemIds;
private String status;
public Order toEntity(){
return Order.builder()
.orderItemIds(this.orderItemIds)
.status(OrderStatus.valueOf(this.status))
.build();
}
}@Getter
@NoArgsConstructor
@AllArgsConstructor
public class OrderResDto {
private Long orderId;
private List<Long> orderItemIds;
private String status;
}
public enum OrderStatus {
CREATED, PAID, SHIPPED, COMPLETED, CANCELLED
}@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@ElementCollection
@CollectionTable(name = "order_items", joinColumns = @JoinColumn(name = "order_id"))
@Column(name = "order_item_id")
private List<Long> orderItemIds;
@CreatedDate
private LocalDateTime createdAt;
private String createdBy;
@LastModifiedDate
private LocalDateTime updatedAt;
private String updatedBy;
private LocalDateTime deletedAt;
private String deletedBy;
public void setCreated(String userId){
this.createdBy = userId;
this.updatedBy = userId; // LastModifiedDate는 생성시에도 값이 입력 됨
}
public void setDeleted(String userId){
this.deletedBy = userId;
this.deletedAt = LocalDateTime.now();
}
public void updateOrder(List<Long> orderItemIds, OrderStatus status, String userId) {
this.orderItemIds = orderItemIds;
this.status = status;
this.updatedBy = userId;
}
public OrderResDto toResDto(){
return new OrderResDto(
this.id,
this.orderItemIds,
this.status.toString()
);
}
}@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProductResDto {
private Long id;
private String name;
private String description;
private Integer price;
private Integer quantity;
}
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/api/products/{productId}")
ProductResDto getProductById(@PathVariable("productId") Long productId);
@PutMapping("/api/products/{productId}/reduceQuantity")
void reduceQuantity(@PathVariable Long productId,
@RequestParam Integer quantity,
@RequestHeader(value = "X-User-Id") String userId);
}
public interface OrderRepository extends JpaRepository<Order, Long> {
}@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {
private final OrderRepository orderRepository;
private final ProductClient productClient;
@Transactional
public OrderResDto createOrder(OrderReqDto requestDto, String userId){
for( Long productId : requestDto.getOrderItemIds()){
ProductResDto product = productClient.getProductById(productId);
if(product == null || product.getQuantity() < 1){
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Product is sold out");
}
}
for(Long productId : requestDto.getOrderItemIds()){
productClient.reduceQuantity(productId,1, userId);
}
Order order = requestDto.toEntity();
order.setCreated(userId);
return orderRepository.save(order).toResDto();
}
public OrderResDto getOrderById(Long orderId){
Order order = orderRepository.findById(orderId)
.filter(o -> o.getDeletedAt() == null)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Order not found or has been deleted"));
return order.toResDto();
}
@Transactional
public OrderResDto updateOrder(Long orderId, OrderReqDto requestDto, String userId) {
Order order = orderRepository.findById(orderId)
.filter(o -> o.getDeletedAt() == null)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Order not found or has been deleted"));
order.updateOrder(requestDto.getOrderItemIds(), OrderStatus.valueOf(requestDto.getStatus()) ,userId);
return order.toResDto();
}
@Transactional
public void deleteOrder(Long orderId, String userId) {
Order order = orderRepository.findById(orderId)
.filter(o -> o.getDeletedAt() == null)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Order not found or has been deleted"));
order.setDeleted(userId);
}
}@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
/*
주문 등록
path /api/orders
Request
header X-User-Id : userId
body OrderReqDto
Response
OrderResDto
*/
@PostMapping
public ResponseEntity<OrderResDto> createProduct(@RequestBody OrderReqDto reqDto,
@RequestHeader(value = "X-User-Id") String userId) {
OrderResDto res = orderService.createOrder(reqDto, userId);
return ResponseEntity.ok(res);
}
/*
주문 단건 조회
Request
Path /api/orders/{orderId}
Response
OrderResDto
*/
@GetMapping("/{orderId}")
public ResponseEntity<OrderResDto> getProductById(@PathVariable Long orderId) {
OrderResDto res = orderService.getOrderById(orderId);
return ResponseEntity.ok(res);
}
/*
주문 정보 업데이트
path /api/orders/{orderId}
Request
header X-User-Id : userId
body OrderReqDto
Response
OrderResDto
*/
@PutMapping("/{orderId}")
public ResponseEntity<OrderResDto> updateOrder(@PathVariable Long orderId,
@RequestBody OrderReqDto reqDto, @RequestHeader(value = "X-User-Id") String userId) {
OrderResDto res = orderService.updateOrder(orderId, reqDto, userId);
return ResponseEntity.ok(res);
}
/*
주문 삭제
path /api/orders/{orderId}
Request
header X-User-Id : userId
Response
message
*/
@DeleteMapping("/{orderId}")
public ResponseEntity<String> deleteOrder(@PathVariable Long orderId,
@RequestHeader(value = "X-User-Id") String userId) {
orderService.deleteOrder(orderId, userId);
return ResponseEntity.ok("Deleted order successfully");
}
}
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.hsqldb:hsqldb'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}spring:
application:
name: auth-service
server:
port: 19095
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
service:
jwt:
access-expiration: 3600000
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserReqDto {
private String userId;
private String userName;
private String password;
// password 암호화 때문에 toEntity 작성 X
}@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserResDto {
private String userId;
private String userName;
}@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SignInReqDto {
private String userId;
private String password;
}@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SignInResDto {
String accessToken;
}public enum UserRole {
MEMBER, MANAGER
}@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "users")
public class User {
@Id
private String userId;
private String userName;
private String password;
private UserRole userRole;
public UserResDto toResDto(){
return new UserResDto(this.userId,this.userName);
}
}public interface UserRepository extends JpaRepository<User, String> {
}@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final String[] AUTH_WHITELIST = {
"/api/auth/**"
};
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize-> authorize
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.formLogin(form -> form.disable())
.build();
}
}@Service
@Transactional
public class UserService {
@Value("auth-service")
private String issuer;
@Value("${service.jwt.access-expiration}")
private Long accessExpiration;
private final SecretKey secretKey;
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public UserService(@Value("${service.jwt.secret-key}") String secretKey,
PasswordEncoder passwordEncoder,
UserRepository userRepository) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
this.passwordEncoder = passwordEncoder;
this.userRepository = userRepository;
}
public SignInResDto createAccessToken(String userId, UserRole role){
return new SignInResDto(
Jwts.builder()
.claim("USER_ID", userId)
.claim("USER_ROLE", role.toString())
.issuer(issuer)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + accessExpiration))
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact()
);
}
public UserResDto signUp(UserReqDto requestDto){
User user = User.builder()
.userId(requestDto.getUserId())
.userName(requestDto.getUserName())
.password(passwordEncoder.encode(requestDto.getPassword()))
.userRole(UserRole.MEMBER)
.build();
userRepository.save(user);
return user.toResDto();
}
public SignInResDto signIn(SignInReqDto requestDto){
User user = userRepository.findById(requestDto.getUserId())
.orElseThrow(() -> new IllegalArgumentException("Invalid user ID or password"));
if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
throw new IllegalArgumentException("Invalid user ID or password");
}
return createAccessToken(user.getUserId(), user.getUserRole());
}
}@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/signIn")
public ResponseEntity<?> createAuthenticationToken(@RequestBody SignInReqDto requestDto){
SignInResDto res = userService.signIn(requestDto);
return ResponseEntity.ok(res);
}
@PostMapping("/signUp")
public ResponseEntity<?> signUp(@RequestBody UserReqDto requestDto) {
UserResDto res = userService.signUp(requestDto);
return ResponseEntity.ok(res);
}
}의존성
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
application.yml
server:
port: 19091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
application:
name: gateway-service # 애플리케이션 이름을 'gateway-service'로 설정
cloud:
gateway:
routes: # Spring Cloud Gateway의 라우팅 설정
- id: order-service # 라우트 식별자
uri: lb://order-service # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/api/orders/** # /order/** 경로로 들어오는 요청을 이 라우트로 처리
- id: product-service # 라우트 식별자
uri: lb://product-service # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/api/products/** # /product/** 경로로 들어오는 요청을 이 라우트로 처리
- id: auth-service # 라우트 식별자
uri: lb://auth-service # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/api/auth/** # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
service:
jwt:
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정
Gateway Project
@Component
@Slf4j
public class CustomPreFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
log.info("Request URI : " + request.getURI());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}@Component
@Slf4j
public class CustomPostFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(()->{
ServerHttpResponse response = exchange.getResponse();
log.info("Response Status Code : " + response.getStatusCode());
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}@Component
@Slf4j
public class JwtAuthFilter implements GlobalFilter {
@Value("${service.jwt.secret-key}")
private String secretKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if(path.equals("/api/auth/signIn") || path.equals("/api/auth/signUp")) {
return chain.filter(exchange); // 로그인 회원가입 경로 필터를 적용 X
}
String token = extractToken(exchange);
if(token == null || !validateToken(token, exchange)){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
private String extractToken(ServerWebExchange exchange) {
String header = exchange.getRequest().getHeaders().getFirst("Authorization");
if(header != null && header.startsWith("Bearer ")){
return header.substring(7);
}
return null;
}
private boolean validateToken(String token, ServerWebExchange exchange) {
try {
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
Jws<Claims> claimsJws = Jwts.parser()
.verifyWith(key)
.build().parseSignedClaims(token);
log.info("payload : " + claimsJws.getPayload().toString());
exchange.getRequest().mutate()
.header("X-User-Id", claimsJws.getPayload().get("USER_ID").toString())
.header("X-User-Role", claimsJws.getPayload().get("USER_ROLE").toString())
.build();
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.", e);
}
return false;
}
}
Querydsl을 사용해본 적이 없으므로 공부 후 내일 적용 예정
하나의 Repo에서 Front, Back 모두 개발 중
Front 배포를 Vercel을 통해서 하는 중
Ignored Build Step에서 Only build production을 설정했지만 계속 preview environment를 빌드 시도하는 이슈 발생
빌드 로그
Error: The specified Root Directory "front/gethertube" does not exist. Please update your Project Settings. Learn More: https://vercel.com/docs/build-step#root-directory
BE 브랜치에서는 Front 배포를 위해 설정한 Root Directory가 존재하지 않아 Ignored Build Step 설정이 적용되기 전에 에러가 발생하는 것으로 추정
BE 브랜치에 .gitkeep을 활용해 front/gethertube Root Directory 생성

빨간 X 표시 때문에 거슬려 하다가 드디어 해결!!!
@Indexed(unique = true) 을 통해 unique 설정을 해줬음에도 불구하고 적용 안되는 현상 발생