From b8193138ef51f3df4b9a86a7adcf393b521a6a3c Mon Sep 17 00:00:00 2001 From: eedo_y Date: Fri, 30 May 2025 19:45:16 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20feat/ZIP-84=20:=20=EC=97=98?= =?UTF-8?q?=EB=9D=BC=EC=8A=A4=ED=8B=B1=20=EC=84=9C=EC=B9=98=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../config/external/ElasticSearchConfig.java | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java diff --git a/build.gradle b/build.gradle index b165a62..47645f2 100644 --- a/build.gradle +++ b/build.gradle @@ -152,6 +152,9 @@ dependencies { // AOP implementation 'org.springframework.boot:spring-boot-starter-aop' + // ELK + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + // Test 관련 annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java new file mode 100644 index 0000000..75b31f8 --- /dev/null +++ b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java @@ -0,0 +1,25 @@ +package com.zipte.platform.core.config.external; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; + +@Configuration +@Profile("local") +@EnableElasticsearchRepositories(basePackages = "com.zipte.platform.server.adapter.out.external.elk") +public class ElasticSearchConfig extends ElasticsearchConfiguration { + + @Value("${elasticsearch.host}") + private String host; + + @Override + public ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() + .connectedTo(host) + .build(); + } + +} From 45430d1e8b3ecbf9a046f135439dfcb1202aac01 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Fri, 30 May 2025 19:46:05 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8=20feat/ZIP-84=20:=20=EC=95=84?= =?UTF-8?q?=ED=8C=8C=ED=8A=B8=EB=B3=84=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/server/adapter/in/web/QaApi.java | 15 ++++ .../in/web/dto/response/QuestionResponse.java | 9 +++ .../out/QuestionPersistenceAdapter.java | 35 ++++++++- .../elk/community/QuestionELKDocument.java | 77 +++++++++++++++++++ .../elk/community/QuestionELKRepository.java | 18 +++++ .../in/community/QuestionUseCase.java | 8 ++ .../out/community/QuestionPort.java | 9 ++- .../application/service/QuestionService.java | 10 +++ .../elasticsearch/question-mapping.json | 29 +++++++ .../elasticsearch/question-setting.json | 9 +++ 10 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java create mode 100644 src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKRepository.java create mode 100644 src/main/resources/elasticsearch/question-mapping.json create mode 100644 src/main/resources/elasticsearch/question-setting.json diff --git a/src/main/java/com/zipte/platform/server/adapter/in/web/QaApi.java b/src/main/java/com/zipte/platform/server/adapter/in/web/QaApi.java index 44fb0f7..7ce991a 100644 --- a/src/main/java/com/zipte/platform/server/adapter/in/web/QaApi.java +++ b/src/main/java/com/zipte/platform/server/adapter/in/web/QaApi.java @@ -8,9 +8,11 @@ import com.zipte.platform.server.adapter.in.web.dto.response.QuestionAnswerDetailResponse; import com.zipte.platform.server.adapter.in.web.dto.request.QuestionRequest; import com.zipte.platform.server.adapter.in.web.dto.response.QuestionAnswerListResponse; +import com.zipte.platform.server.adapter.in.web.dto.response.QuestionResponse; import com.zipte.platform.server.adapter.in.web.swagger.QaApiSpec; import com.zipte.platform.server.application.in.community.AnswerUseCase; import com.zipte.platform.server.application.in.community.QuestionUseCase; +import com.zipte.platform.server.domain.community.Question; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -87,6 +89,19 @@ public ApiResponse deleteQuestion( } + @GetMapping("/question") + public ApiResponse> getQuestions( + @RequestParam(required = true) String kaptCode, + @RequestParam(required = true) String title + ) { + List questionList = questionService.loadQuestionsByKeyword(kaptCode, title); + + List responses = QuestionResponse.from(questionList); + + return ApiResponse.ok(responses); + } + + @PostMapping("/answer") public ApiResponse createAnswer( @RequestBody @Valid AnswerRequest request, diff --git a/src/main/java/com/zipte/platform/server/adapter/in/web/dto/response/QuestionResponse.java b/src/main/java/com/zipte/platform/server/adapter/in/web/dto/response/QuestionResponse.java index 4babb88..7008526 100644 --- a/src/main/java/com/zipte/platform/server/adapter/in/web/dto/response/QuestionResponse.java +++ b/src/main/java/com/zipte/platform/server/adapter/in/web/dto/response/QuestionResponse.java @@ -3,6 +3,8 @@ import com.zipte.platform.server.domain.community.Question; import lombok.Builder; +import java.util.*; + @Builder public record QuestionResponse (Long id, String kaptCode, String title, String content) { @@ -17,4 +19,11 @@ public static QuestionResponse from(Question question) { .build(); } + public static List from(List questions) { + + return questions.stream() + .map(QuestionResponse::from) + .toList(); + } + } diff --git a/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java b/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java index 5003ccb..2d223a3 100644 --- a/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java +++ b/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java @@ -1,5 +1,7 @@ package com.zipte.platform.server.adapter.out; +import com.zipte.platform.server.adapter.out.external.elk.community.QuestionELKDocument; +import com.zipte.platform.server.adapter.out.external.elk.community.QuestionELKRepository; import com.zipte.platform.server.adapter.out.jpa.community.QuestionJpaEntity; import com.zipte.platform.server.adapter.out.jpa.community.QuestionJpaRepository; import com.zipte.platform.server.application.out.community.QuestionPort; @@ -9,6 +11,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; +import java.util.List; import java.util.Optional; @Component @@ -17,15 +20,26 @@ public class QuestionPersistenceAdapter implements QuestionPort { private final QuestionJpaRepository repository; + /// 엘라스틱 저장 의존성 추가 + private final QuestionELKRepository elkRepository; + + /// 저장 @Override public Question save(Question question) { + /// JPA 관련 var entity = QuestionJpaEntity.from(question); - - return repository.save(entity) + Question domain = repository.save(entity) .toDomain(); + + // ELK 관련 + QuestionELKDocument elkDocument = QuestionELKDocument.from(domain); + elkRepository.save(elkDocument); + + return domain; } + /// 조회 @Override public Optional loadQuestion(Long id) { return repository.findById(id) @@ -38,11 +52,28 @@ public Page loadQuestionsByKaptCode(String kaptCode, Pageable pageable .map(QuestionJpaEntity::toDomain); } + @Override + public Page loadQuestionsByKeyword(String kaptCode, String keyword, Pageable pageable) { + return elkRepository.findByTitleAndKaptCode(keyword, kaptCode, pageable) + .map(QuestionELKDocument::toDomain); + } + + @Override + public List loadQuestionsByKeyword(String kaptCode, String keyword) { + return elkRepository.findByTitle(keyword).stream() + .map(QuestionELKDocument::toDomain) + .toList(); + } + + + /// 삭제 @Override public void deleteQuestionById(Long id) { repository.deleteById(id); } + + /// 체크 @Override public boolean checkExistQuestion(Long questionId) { return repository.existsById(questionId); diff --git a/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java b/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java new file mode 100644 index 0000000..70a856a --- /dev/null +++ b/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java @@ -0,0 +1,77 @@ +package com.zipte.platform.server.adapter.out.external.elk.community; + +import com.zipte.platform.server.domain.community.Question; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.elasticsearch.annotations.*; + +import java.time.LocalDateTime; + +@Getter +@Document(indexName = "question", createIndex = true) +@Setting(settingPath = "elasticsearch/question-setting.json") +@Mapping(mappingPath = "elasticsearch/question-mapping.json") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class QuestionELKDocument { + + @Id + @Field(type = FieldType.Long) + private Long id; + + @Field(type = FieldType.Long) + private Long userId; + + @Field(type = FieldType.Text) + private String kaptCode; + + @Field(type = FieldType.Text) + private String title; + + @Field(type = FieldType.Text) + private String content; + + @Field(type = FieldType.Date, format = {DateFormat.date_hour_minute_second_millis, DateFormat.epoch_millis}) + private LocalDateTime createdAt; + + @Field(type = FieldType.Date, format = {DateFormat.date_hour_minute_second_millis, DateFormat.epoch_millis}) + private LocalDateTime updatedAt; + + @Builder + public QuestionELKDocument(Long id, Long userId, String kaptCode, String title, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.userId = userId; + this.kaptCode = kaptCode; + this.title = title; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /// 정적 팩토리 메서드 + public static QuestionELKDocument from(Question question) { + return QuestionELKDocument.builder() + .id(question.getId()) + .kaptCode(question.getKaptCode()) + .title(question.getTitle()) + .content(question.getContent()) + .createdAt(question.getCreatedAt()) + .updatedAt(question.getUpdatedAt()) + .build(); + } + + /// 도메인 수정 + public Question toDomain() { + return Question.builder() + .id(id) + .kaptCode(kaptCode) + .title(title) + .content(content) + .createdAt(createdAt) + .updatedAt(updatedAt) + .build(); + } + +} diff --git a/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKRepository.java b/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKRepository.java new file mode 100644 index 0000000..0afd220 --- /dev/null +++ b/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKRepository.java @@ -0,0 +1,18 @@ +package com.zipte.platform.server.adapter.out.external.elk.community; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import java.util.List; + +public interface QuestionELKRepository extends ElasticsearchRepository { + + Page findByTitleAndKaptCode(String title, String kaptCode, Pageable pageable); + + List findByTitle(String title); + + List findByTitleAndKaptCode(String title, String kaptCode); + + +} diff --git a/src/main/java/com/zipte/platform/server/application/in/community/QuestionUseCase.java b/src/main/java/com/zipte/platform/server/application/in/community/QuestionUseCase.java index c0b6cab..a916d7b 100644 --- a/src/main/java/com/zipte/platform/server/application/in/community/QuestionUseCase.java +++ b/src/main/java/com/zipte/platform/server/application/in/community/QuestionUseCase.java @@ -7,6 +7,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.List; + public interface QuestionUseCase { /* @@ -23,6 +25,12 @@ public interface QuestionUseCase { /// 질문 목록 조회하기 Page loadQuestions(String kaptCode, Pageable pageable); + /// 특정 키워드 조회 + Page loadQuestionsByKeyword(String kaptCode, String keyword, Pageable pageable); + + + List loadQuestionsByKeyword(String kaptCode, String keyword); + /// 질문 삭제하기 void deleteQuestion(Long questionId, Long userId); diff --git a/src/main/java/com/zipte/platform/server/application/out/community/QuestionPort.java b/src/main/java/com/zipte/platform/server/application/out/community/QuestionPort.java index be5779b..b2f3fcf 100644 --- a/src/main/java/com/zipte/platform/server/application/out/community/QuestionPort.java +++ b/src/main/java/com/zipte/platform/server/application/out/community/QuestionPort.java @@ -4,7 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.util.Optional; +import java.util.*; public interface QuestionPort { @@ -15,9 +15,14 @@ public interface QuestionPort { // 상세 조회하기 Optional loadQuestion(Long id); - // 아파트 이름별, 질문 목록 조회 + // 아파트별, 질문 목록 조회 Page loadQuestionsByKaptCode(String kaptCode, Pageable pageable); + // 아파트별, 키워드 기반 조회 + Page loadQuestionsByKeyword(String kaptCode, String keyword, Pageable pageable); + + List loadQuestionsByKeyword(String kaptCode, String keyword); + /// 삭제 void deleteQuestionById(Long id); diff --git a/src/main/java/com/zipte/platform/server/application/service/QuestionService.java b/src/main/java/com/zipte/platform/server/application/service/QuestionService.java index 8d67c58..a245925 100644 --- a/src/main/java/com/zipte/platform/server/application/service/QuestionService.java +++ b/src/main/java/com/zipte/platform/server/application/service/QuestionService.java @@ -99,6 +99,16 @@ public Page loadQuestions(String kaptCode, Pageable return new PageImpl<>(responseList, pageable, questions.getTotalElements()); } + @Override + public Page loadQuestionsByKeyword(String kaptCode, String keyword, Pageable pageable) { + return null; + } + + @Override + public List loadQuestionsByKeyword(String kaptCode, String keyword) { + return questionPort.loadQuestionsByKeyword(kaptCode, keyword); + } + /// 질문 삭제하기 @Override public void deleteQuestion(Long id, Long userId) { diff --git a/src/main/resources/elasticsearch/question-mapping.json b/src/main/resources/elasticsearch/question-mapping.json new file mode 100644 index 0000000..ad24417 --- /dev/null +++ b/src/main/resources/elasticsearch/question-mapping.json @@ -0,0 +1,29 @@ +{ + "properties": { + "id": { + "type": "long" + }, + "userId": { + "type": "long" + }, + "kaptCode": { + "type": "text" + }, + "title": { + "type": "text", + "analyzer": "korean" + }, + "content": { + "type": "text", + "analyzer": "korean" + }, + "createdAt": { + "type": "date", + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS||epoch_millis" + }, + "updatedAt": { + "type": "date", + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS||epoch_millis" + } + } +} diff --git a/src/main/resources/elasticsearch/question-setting.json b/src/main/resources/elasticsearch/question-setting.json new file mode 100644 index 0000000..63fecb1 --- /dev/null +++ b/src/main/resources/elasticsearch/question-setting.json @@ -0,0 +1,9 @@ +{ + "analysis": { + "analyzer": { + "korean": { + "type": "nori" + } + } + } +} From 25455966a41e25a74a76fe845ab3cb15a87e488b Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 5 Jun 2025 15:46:03 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8=20feat/ZIP-84=20:=20=EC=95=84?= =?UTF-8?q?=ED=8C=8C=ED=8A=B8=EB=B3=84=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B5=AC=ED=98=84=20-=20=EC=95=84?= =?UTF-8?q?=ED=8C=8C=ED=8A=B8=20=EC=BD=94=EB=93=9C=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/adapter/out/QuestionPersistenceAdapter.java | 6 +++++- .../out/external/elk/community/QuestionELKDocument.java | 2 +- .../server/application/service/QuestionService.java | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java b/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java index 2d223a3..ed52a5c 100644 --- a/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java +++ b/src/main/java/com/zipte/platform/server/adapter/out/QuestionPersistenceAdapter.java @@ -60,7 +60,7 @@ public Page loadQuestionsByKeyword(String kaptCode, String keyword, Pa @Override public List loadQuestionsByKeyword(String kaptCode, String keyword) { - return elkRepository.findByTitle(keyword).stream() + return elkRepository.findByTitleAndKaptCode(keyword, kaptCode).stream() .map(QuestionELKDocument::toDomain) .toList(); } @@ -69,7 +69,11 @@ public List loadQuestionsByKeyword(String kaptCode, String keyword) { /// 삭제 @Override public void deleteQuestionById(Long id) { + /// DB에서 삭제 repository.deleteById(id); + + /// ELK에서 삭제 + elkRepository.deleteById(id); } diff --git a/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java b/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java index 70a856a..9981f58 100644 --- a/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java +++ b/src/main/java/com/zipte/platform/server/adapter/out/external/elk/community/QuestionELKDocument.java @@ -40,7 +40,7 @@ public class QuestionELKDocument { private LocalDateTime updatedAt; @Builder - public QuestionELKDocument(Long id, Long userId, String kaptCode, String title, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { + private QuestionELKDocument(Long id, Long userId, String kaptCode, String title, String content, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.userId = userId; this.kaptCode = kaptCode; diff --git a/src/main/java/com/zipte/platform/server/application/service/QuestionService.java b/src/main/java/com/zipte/platform/server/application/service/QuestionService.java index a245925..0ef4bf1 100644 --- a/src/main/java/com/zipte/platform/server/application/service/QuestionService.java +++ b/src/main/java/com/zipte/platform/server/application/service/QuestionService.java @@ -106,6 +106,7 @@ public Page loadQuestionsByKeyword(String kaptCode, String keyword, Pa @Override public List loadQuestionsByKeyword(String kaptCode, String keyword) { + /// 특정 아파트에서 키워드를 바탕으로 조회하도록 진행해야한다. return questionPort.loadQuestionsByKeyword(kaptCode, keyword); } From ae532f2bb53f190a8aac8921281de24adc7d3ba2 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Sat, 28 Jun 2025 15:28:56 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8=20feat/ZIP-84=20:=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=ED=95=A0=20ELK=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose/docker-compose.yml | 235 +++++++++++++++++- compose/es/es-entrypoint.sh | 12 + compose/logstash/logstash.conf | 24 ++ .../config/external/ElasticSearchConfig.java | 1 + .../external/ElasticSearchDevConfig.java | 31 +++ .../adapter/in/web/swagger/QaApiSpec.java | 17 ++ 6 files changed, 314 insertions(+), 6 deletions(-) create mode 100644 compose/es/es-entrypoint.sh create mode 100644 compose/logstash/logstash.conf create mode 100644 src/main/java/com/zipte/platform/core/config/external/ElasticSearchDevConfig.java diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index b1dfb89..de0c9f4 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -1,4 +1,79 @@ +volumes: + certs: + driver: local + esdata01: + driver: local + kibanadata: + driver: local + metricbeatdata01: + driver: local + filebeatdata01: + driver: local + logstashdata01: + driver: local + +networks: + backend-bridge: + driver: bridge + services: + setup: + image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} + container_name: zipte-setup + volumes: + - certs:/usr/share/elasticsearch/config/certs + user: "0" + command: > + bash -c ' + if [ x${ELASTIC_PASSWORD} == x ]; then + echo "Set the ELASTIC_PASSWORD environment variable in the .env file"; + exit 1; + elif [ x${KIBANA_PASSWORD} == x ]; then + echo "Set the KIBANA_PASSWORD environment variable in the .env file"; + exit 1; + fi; + if [ ! -f config/certs/ca.zip ]; then + echo "Creating CA"; + bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; + unzip config/certs/ca.zip -d config/certs; + fi; + if [ ! -f config/certs/certs.zip ]; then + echo "Creating certs"; + echo -ne \ + "instances:\n"\ + " - name: es01\n"\ + " dns:\n"\ + " - es01\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + " - name: kibana\n"\ + " dns:\n"\ + " - kibana\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + > config/certs/instances.yml; + bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; + unzip config/certs/certs.zip -d config/certs; + fi; + echo "Setting file permissions" + chown -R root:root config/certs; + find . -type d -exec chmod 750 \{\} \;; + find . -type f -exec chmod 640 \{\} \;; + echo "Waiting for Elasticsearch availability"; + until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done; + echo "Setting kibana_system password"; + until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done; + echo "All done!"; + ' + healthcheck: + test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"] + interval: 1s + timeout: 5s + retries: 120 + networks: + - backend-bridge spring: image: zipte/server:latest @@ -35,9 +110,8 @@ services: networks: - backend-bridge - # Nginx Proxy - nginx: # nginx 서비스 정의 - image: nginx:latest # 사용할 Docker 이미지 + nginx: + image: nginx:latest container_name: zipte-nginx volumes: - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf @@ -65,6 +139,155 @@ services: - backend-bridge entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" -networks: - backend-bridge: - driver: bridge + es01: + container_name: zipte-es + depends_on: + setup: + condition: service_healthy + image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} + labels: + co.elastic.logs/module: elasticsearch + volumes: + - certs:/usr/share/elasticsearch/config/certs + - esdata01:/usr/share/elasticsearch/data + - ./es-entrypoint.sh:/usr/local/bin/es-entrypoint.sh + entrypoint: [ "/usr/local/bin/es-entrypoint.sh" ] + ports: + - ${ES_PORT}:9200 + environment: + - node.name=es01 + - cluster.name=${CLUSTER_NAME} + - discovery.type=single-node + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + - bootstrap.memory_lock=true + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.key=certs/es01/es01.key + - xpack.security.http.ssl.certificate=certs/es01/es01.crt + - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.enabled=true + - xpack.security.transport.ssl.key=certs/es01/es01.key + - xpack.security.transport.ssl.certificate=certs/es01/es01.crt + - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.verification_mode=certificate + - xpack.license.self_generated.type=${LICENSE} + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", + ] + interval: 10s + timeout: 10s + retries: 120 + networks: + - backend-bridge + + kibana: + container_name: zipte-kibana + depends_on: + es01: + condition: service_healthy + image: docker.elastic.co/kibana/kibana:${STACK_VERSION} + labels: + co.elastic.logs/module: kibana + volumes: + - certs:/usr/share/kibana/config/certs + - kibanadata:/usr/share/kibana/data + ports: + - ${KIBANA_PORT}:5601 + environment: + - SERVERNAME=kibana + - ELASTICSEARCH_HOSTS=https://es01:9200 + - ELASTICSEARCH_USERNAME=kibana_system + - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD} + - ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=config/certs/ca/ca.crt + - XPACK_SECURITY_ENCRYPTIONKEY=${ENCRYPTION_KEY} + - XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${ENCRYPTION_KEY} + - XPACK_REPORTING_ENCRYPTIONKEY=${ENCRYPTION_KEY} + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'", + ] + interval: 10s + timeout: 10s + retries: 120 + networks: + - backend-bridge + + metricbeat01: + container_name: zipte-metricbeat + depends_on: + es01: + condition: service_healthy + kibana: + condition: service_healthy + image: docker.elastic.co/beats/metricbeat:${STACK_VERSION} + user: root + volumes: + - certs:/usr/share/metricbeat/certs + - metricbeatdata01:/usr/share/metricbeat/data + - "./metricbeat/metricbeat.yml:/usr/share/metricbeat/metricbeat.yml:ro" + - "/var/run/docker.sock:/var/run/docker.sock:ro" + - "/sys/fs/cgroup:/hostfs/sys/fs/cgroup:ro" + - "/proc:/hostfs/proc:ro" + - "/:/hostfs:ro" + environment: + - ELASTIC_USER=elastic + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + - ELASTIC_HOSTS=https://es01:9200 + - KIBANA_HOSTS=http://kibana:5601 + - LOGSTASH_HOSTS=http://logstash01:9600 + networks: + - backend-bridge + + filebeat01: + container_name: zipte-filebeat + depends_on: + es01: + condition: service_healthy + image: docker.elastic.co/beats/filebeat:${STACK_VERSION} + user: root + volumes: + - certs:/usr/share/filebeat/certs + - filebeatdata01:/usr/share/filebeat/data + - "./filebeat/filebeat_ingest_data/:/usr/share/filebeat/ingest_data/" + - "./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro" + - "/var/lib/docker/containers:/var/lib/docker/containers:ro" + - "/var/run/docker.sock:/var/run/docker.sock:ro" + environment: + - ELASTIC_USER=elastic + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + - ELASTIC_HOSTS=https://es01:9200 + - KIBANA_HOSTS=http://kibana:5601 + - LOGSTASH_HOSTS=http://logstash01:9600 + networks: + - backend-bridge + + logstash01: + container_name: zipte-logstash + depends_on: + es01: + condition: service_healthy + kibana: + condition: service_healthy + image: docker.elastic.co/logstash/logstash:${STACK_VERSION} + labels: + co.elastic.logs/module: logstash + user: root + volumes: + - certs:/usr/share/logstash/certs + - logstashdata01:/usr/share/logstash/data + - "./logstash/logstash_ingest_data/:/usr/share/logstash/ingest_data/" + - "./logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro" + environment: + - xpack.monitoring.enabled=false + - ELASTIC_USER=elastic + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + - ELASTIC_HOSTS=https://es01:9200 diff --git a/compose/es/es-entrypoint.sh b/compose/es/es-entrypoint.sh new file mode 100644 index 0000000..ad8810f --- /dev/null +++ b/compose/es/es-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# 이미 설치되어 있지 않으면 설치 +if [ ! -d "/usr/share/elasticsearch/plugins/analysis-nori" ]; then + echo "Nori plugin not found, installing..." + elasticsearch-plugin install analysis-nori --batch +else + echo "Nori plugin already installed" +fi + +# 원래 entrypoint 실행 +exec /bin/tini -- /usr/local/bin/docker-entrypoint.sh "$@" diff --git a/compose/logstash/logstash.conf b/compose/logstash/logstash.conf new file mode 100644 index 0000000..6f65bad --- /dev/null +++ b/compose/logstash/logstash.conf @@ -0,0 +1,24 @@ +input { + file { + #https://www.elastic.co/guide/en/logstash/current/plugins-inputs-file.html + #default is TAIL which assumes more data will come into the file. + #change to mode => "read" if the file is a compelte file. by default, the file will be removed once reading is complete -- backup your files if you need them. + mode => "tail" + path => "/usr/share/logstash/ingest_data/*" + } +} + + +filter { +} + + +output { + elasticsearch { + index => "logstash-%{+YYYY.MM.dd}" + hosts=> "${ELASTIC_HOSTS}" + user=> "${ELASTIC_USER}" + password=> "${ELASTIC_PASSWORD}" + cacert=> "certs/ca/ca.crt" + } +} diff --git a/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java index 75b31f8..1adc963 100644 --- a/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java +++ b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java @@ -19,6 +19,7 @@ public class ElasticSearchConfig extends ElasticsearchConfiguration { public ClientConfiguration clientConfiguration() { return ClientConfiguration.builder() .connectedTo(host) + .usingSsl(false) .build(); } diff --git a/src/main/java/com/zipte/platform/core/config/external/ElasticSearchDevConfig.java b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchDevConfig.java new file mode 100644 index 0000000..0602ae8 --- /dev/null +++ b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchDevConfig.java @@ -0,0 +1,31 @@ +package com.zipte.platform.core.config.external; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; + +@Configuration +@Profile("dev") +@EnableElasticsearchRepositories(basePackages = "com.zipte.platform.server.adapter.out.external.elk") +public class ElasticSearchDevConfig extends ElasticsearchConfiguration { + + @Value("${elasticsearch.host}") + private String host; + + @Value("${elasticsearch.user_name}") + private String username; + + @Value("${elasticsearch.user_password}") + private String password; + + @Override + public ClientConfiguration clientConfiguration() { + return ClientConfiguration.builder() + .connectedTo(host) + .withBasicAuth(username, password) + .build(); + } +} diff --git a/src/main/java/com/zipte/platform/server/adapter/in/web/swagger/QaApiSpec.java b/src/main/java/com/zipte/platform/server/adapter/in/web/swagger/QaApiSpec.java index d6eee90..3f94a01 100644 --- a/src/main/java/com/zipte/platform/server/adapter/in/web/swagger/QaApiSpec.java +++ b/src/main/java/com/zipte/platform/server/adapter/in/web/swagger/QaApiSpec.java @@ -8,6 +8,7 @@ import com.zipte.platform.server.adapter.in.web.dto.request.QuestionRequest; import com.zipte.platform.server.adapter.in.web.dto.response.QuestionAnswerDetailResponse; import com.zipte.platform.server.adapter.in.web.dto.response.QuestionAnswerListResponse; +import com.zipte.platform.server.adapter.in.web.dto.response.QuestionResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -18,6 +19,9 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; @Tag(name = "Q&A API", description = "Q&A 관련 API") public interface QaApiSpec { @@ -40,6 +44,19 @@ ApiResponse createQuestion( @Parameter(hidden = true) PrincipalDetails principalDetails); + /// 질문 검색 + @Operation( + summary = "질문 검색", + description = "ES를 통해서 질문에 대한 키워드를 바탕으로 질문을 검색합니다." + ) + ApiResponse> getQuestions( + @Parameter(description = "아파트 코드", example = "A46393018") + @RequestParam(required = true) String kaptCode, + + @Parameter(description = "검색할 키워드", example = "벌레") + @RequestParam(required = true) String title + ); + @Operation( summary = "질문 목록 조회", From aad9176f633f06ef5881cd8a7202da9c3b5d1db5 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Sat, 28 Jun 2025 15:46:02 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/ZIP-84=20:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20ES?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-test.yml | 30 +++++++++++++++++++ .../config/external/ElasticSearchConfig.java | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 44f1691..e1c4ff7 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -40,6 +40,19 @@ jobs: ports: - 27017:27017 + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + ports: + - 9200:9200 + options: >- + --health-cmd "curl -f http://localhost:9200/_cluster/health || exit 1" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + env: + discovery.type: single-node + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + permissions: contents: write checks: write @@ -108,6 +121,23 @@ jobs: sleep 3 done + - name: Elasticsearch 체크 + run: | + until curl -sSf http://localhost:9200/_cluster/health; do + echo "Waiting for Elasticsearch..." + sleep 5 + done + + + # Nori 플러그인 설치 + - name: Install Nori plugin + run: | + CONTAINER_ID=$(docker ps -q -f ancestor=docker.elastic.co/elasticsearch/elasticsearch:7.17.0) + echo "Elasticsearch container ID: $CONTAINER_ID" + docker exec $CONTAINER_ID bin/elasticsearch-plugin install analysis-nori --batch + docker restart $CONTAINER_ID + sleep 30 + # 2. 빌드 - name: Build with Gradle Wrapper run: ./gradlew clean build diff --git a/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java index 1adc963..66333d9 100644 --- a/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java +++ b/src/main/java/com/zipte/platform/core/config/external/ElasticSearchConfig.java @@ -8,7 +8,7 @@ import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; @Configuration -@Profile("local") +@Profile({"local","test"}) @EnableElasticsearchRepositories(basePackages = "com.zipte.platform.server.adapter.out.external.elk") public class ElasticSearchConfig extends ElasticsearchConfiguration { From d197ea816159781139fdfb12db63e1285e8a3403 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Sat, 28 Jun 2025 15:59:28 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/ZIP-8=20:=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20ES=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-test.yml | 2 +- .github/workflows/dev-ci-cd.yml | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index e1c4ff7..761fb21 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -41,7 +41,7 @@ jobs: - 27017:27017 elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} ports: - 9200:9200 options: >- diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index 6bac232..f87ad3a 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -36,6 +36,19 @@ jobs: ports: - 27017:27017 + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} + ports: + - 9200:9200 + options: >- + --health-cmd "curl -f http://localhost:9200/_cluster/health || exit 1" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + env: + discovery.type: single-node + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + permissions: contents: write checks: write @@ -64,7 +77,7 @@ jobs: mkdir -p src/test/resources echo "${{ secrets.APPLICATION_TEST_YML }}" > ./src/test/resources/application-test.yml - # 체크 + # 3-1. 체크 - name: MySQL 체크 run: | until nc -z localhost 3306; do @@ -87,6 +100,21 @@ jobs: sleep 3 done + - name: Elasticsearch 체크 + run: | + until curl -sSf http://localhost:9200/_cluster/health; do + echo "Waiting for Elasticsearch..." + sleep 5 + done + + # 3-2. Nori 플러그인 설치 + - name: Install Nori plugin + run: | + CONTAINER_ID=$(docker ps -q -f ancestor=docker.elastic.co/elasticsearch/elasticsearch:7.17.0) + echo "Elasticsearch container ID: $CONTAINER_ID" + docker exec $CONTAINER_ID bin/elasticsearch-plugin install analysis-nori --batch + docker restart $CONTAINER_ID + sleep 30 # 4. gradle 환경 설치 - name: Gradle Wrapper 권한 부여 From d568a1ab137697f9c61c6ca081584187fe4ff829 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Sat, 28 Jun 2025 16:03:48 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/ZIP-8=20:=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20ES=20=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-test.yml | 2 +- .github/workflows/dev-ci-cd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 761fb21..c9533ed 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -41,7 +41,7 @@ jobs: - 27017:27017 elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} + image: docker.elastic.co/elasticsearch/elasticsearch:8.7.1 ports: - 9200:9200 options: >- diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index f87ad3a..8a5f9bc 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -37,7 +37,7 @@ jobs: - 27017:27017 elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} + image: docker.elastic.co/elasticsearch/elasticsearch:8.7.1 ports: - 9200:9200 options: >- From 9c4c640e2f076bfbc4cad69cc3a23eda6a1f003d Mon Sep 17 00:00:00 2001 From: eedo_y Date: Sat, 28 Jun 2025 16:07:48 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/ZIP-8=20:=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20ES=20=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95(2)=20-?= =?UTF-8?q?=207.17.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-test.yml | 2 +- .github/workflows/dev-ci-cd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index c9533ed..e1c4ff7 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -41,7 +41,7 @@ jobs: - 27017:27017 elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.7.1 + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 ports: - 9200:9200 options: >- diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index 8a5f9bc..5a214ff 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -37,7 +37,7 @@ jobs: - 27017:27017 elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.7.1 + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 ports: - 9200:9200 options: >-