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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
// 설문 문항·선택지 마스터만 Postgres다(나머지 관리 대상은 Mongo). 스키마 주인은 api-server이고
// 설문 문항·선택지와 성분 카탈로그는 Postgres다(나머지 관리 대상은 Mongo). 스키마 주인은 api-server이고
// 여기서는 ddl-auto=none으로 붙는다 — application.yml 주석 참조.
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.seoulection.admin.ingredient.application.service;

import com.seoulection.admin.ingredient.infrastructure.document.Ingredient;
import com.seoulection.admin.ingredient.infrastructure.repository.IngredientPostgresRepository;
import com.seoulection.admin.ingredient.infrastructure.document.Ingredient.EvidenceView;
import com.seoulection.admin.ingredient.infrastructure.document.Ingredient.EfficacyRangeView;
import com.seoulection.admin.ingredient.infrastructure.repository.PropertyDefinitionView;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Arrays;
import java.util.Map;
import com.seoulection.admin.ingredient.infrastructure.document.Ingredient.ConditionView;

@Service
public class IngredientService {
private final IngredientPostgresRepository repository;

public IngredientService(IngredientPostgresRepository repository) { this.repository = repository; }
public List<Ingredient> getIngredients() { return repository.findAll(); }
public List<PropertyDefinitionView> getPropertyDefinitions() { return repository.findPropertyDefinitions(); }

public Ingredient getIngredient(String id) {
Ingredient ingredient = repository.findById(id);
if (ingredient == null) throw new IllegalArgumentException("성분을 찾을 수 없습니다: " + id);
return ingredient;
}

public void create(String inciName, String displayNameKo, String family,
String aliasesText, String effectsText) {
create(inciName, displayNameKo, family, aliasesText, effectsText, "", "", "", "");
}

public void create(String inciName, String displayNameKo, String family,
String aliasesText, String effectsText,
String propertiesText, String evidenceText, String efficacyRangesText) {
create(inciName, displayNameKo, family, aliasesText, effectsText, propertiesText, evidenceText, efficacyRangesText, "");
}

public void create(String inciName, String displayNameKo, String family,
String aliasesText, String effectsText,
String propertiesText, String evidenceText, String efficacyRangesText, String efficacyConditionsText) {
String id = java.util.UUID.randomUUID().toString();
repository.save(id, inciName.trim(), displayNameKo.trim(), family.trim(),
split(aliasesText), parseEffects(effectsText), parseMap(propertiesText));
repository.replaceEvidence(id, parseEvidence(evidenceText));
repository.replaceRanges(id, parseRanges(efficacyRangesText), parseConditions(efficacyConditionsText));
}

public void update(String id, String inciName, String displayNameKo, String family,
String aliasesText, String effectsText) {
update(id, inciName, displayNameKo, family, aliasesText, effectsText, "", "", "", "");
}

public void update(String id, String inciName, String displayNameKo, String family,
String aliasesText, String effectsText,
String propertiesText, String evidenceText, String efficacyRangesText) {
update(id, inciName, displayNameKo, family, aliasesText, effectsText, propertiesText, evidenceText, efficacyRangesText, "");
}

public void update(String id, String inciName, String displayNameKo, String family,
String aliasesText, String effectsText,
String propertiesText, String evidenceText, String efficacyRangesText, String efficacyConditionsText) {
repository.save(id, inciName.trim(), displayNameKo.trim(), family.trim(),
split(aliasesText), parseEffects(effectsText), parseMap(propertiesText));
if (evidenceText != null && !evidenceText.isBlank()) repository.replaceEvidence(id, parseEvidence(evidenceText));
if (efficacyRangesText != null && !efficacyRangesText.isBlank()) repository.replaceRanges(id, parseRanges(efficacyRangesText), parseConditions(efficacyConditionsText));
}

private List<String> split(String value) {
if (value == null || value.isBlank()) return List.of();
return Arrays.stream(value.split("[,\\n]")).map(String::trim).filter(v -> !v.isBlank()).distinct().toList();
}

private Map<String, String> parseEffects(String value) {
if (value == null || value.isBlank()) return Map.of();
return Arrays.stream(value.split("[,\\n]"))
.map(String::trim).filter(v -> v.contains("="))
.<String[]>map(v -> v.split("=", 2))
.collect(java.util.stream.Collectors.toMap(v -> v[0].trim(), v -> v[1].trim(), (a, b) -> b));
}

private Map<String, String> parseMap(String value) {
return parseEffects(value);
}

private List<EvidenceView> parseEvidence(String value) {
if (value == null || value.isBlank()) return List.of();
return Arrays.stream(value.split("\\n"))
.map(String::trim).filter(v -> !v.isBlank()).map(v -> v.split("\\|", -1))
.filter(v -> v.length >= 5 && !v[1].isBlank())
.map(v -> new EvidenceView(null, v[0], v[1], blank(v, 2), Boolean.parseBoolean(blank(v, 3)),
blank(v, 4), blank(v, 5), Boolean.parseBoolean(blank(v, 6)), blank(v, 7), blank(v, 8), blank(v, 9), blank(v, 10)))
.toList();
}

private List<EfficacyRangeView> parseRanges(String value) {
if (value == null || value.isBlank()) return List.of();
return Arrays.stream(value.split("\\n"))
.map(String::trim).filter(v -> !v.isBlank()).map(v -> v.split("\\|", -1))
.filter(v -> v.length >= 1 && !v[0].isBlank())
.map(v -> new EfficacyRangeView(v[0], blank(v, 1), blank(v, 2), blank(v, 3), blank(v, 4), blank(v, 5), blank(v, 6), longValue(v, 7), blank(v, 8)))
.toList();
}

private List<ConditionView> parseConditions(String value) {
if (value == null || value.isBlank()) return List.of();
return Arrays.stream(value.split("\\n"))
.map(String::trim).filter(v -> !v.isBlank()).map(v -> v.split("\\|", -1))
.filter(v -> v.length >= 2 && !v[0].isBlank() && !v[1].isBlank())
.map(v -> new ConditionView(v[0].trim(), v[1].trim(), blank(v, 5), blank(v, 2), blank(v, 3), blank(v, 4),
blank(v, 6) == null ? "EXACT_VALUE" : blank(v, 6), Boolean.parseBoolean(blank(v, 7))))
.toList();
}

private String blank(String[] values, int index) { return index < values.length && !values[index].isBlank() ? values[index].trim() : null; }
private Long longValue(String[] values, int index) { return blank(values, index) == null ? null : Long.valueOf(values[index].trim()); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.seoulection.admin.ingredient.infrastructure.config;

import com.seoulection.admin.ingredient.infrastructure.document.Ingredient;
import com.seoulection.admin.ingredient.infrastructure.repository.IngredientPostgresRepository;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Map;

@Configuration
public class IngredientSeedConfiguration {
@Bean
ApplicationRunner seedIngredients(IngredientPostgresRepository repository) {
return args -> {
if (!repository.findAll().isEmpty()) return;
List.of(
i("00000000-0000-0000-0000-000000000101","Hyaluronic Acid","Hyaluronic Acid","히알루론산","HYALURONAN",List.of("HA","히알루론산"),List.of("HYALURONIC_ACID_SEARCH"),Map.of("WATER_SCORE","CORE","WRINKLE_SCORE","SUPPORT"),Map.of("SOLUBILITY","WATER_SOLUBLE")),
i("00000000-0000-0000-0000-000000000102","Sodium DNA","Sodium DNA","PDRN","NUCLEOTIDE_DERIVATIVE",List.of("PDRN","소듐 DNA"),List.of("PDRN_SEARCH"),Map.of("WATER_SCORE","SUPPORT","WRINKLE_SCORE","SUPPORT"),Map.of("SOLUBILITY","WATER_SOLUBLE")),
i("00000000-0000-0000-0000-000000000103","Panthenol","Panthenol","판테놀","VITAMIN_B5_DERIVATIVE",List.of("D-Panthenol","판테놀"),List.of("SOOTHING_SEARCH"),Map.of("SENSITIVITY_SCORE","CORE","WATER_SCORE","SUPPORT"),Map.of("SOLUBILITY","WATER_SOLUBLE")),
i("00000000-0000-0000-0000-000000000104","Centella Asiatica","Centella Asiatica","병풀","BOTANICAL_EXTRACT",List.of("Cica","병풀"),List.of("SOOTHING_SEARCH"),Map.of("SENSITIVITY_SCORE","SUPPORT","RED_SPOT_SCORE","SUPPORT"),Map.of()),
i("00000000-0000-0000-0000-000000000105","Retinol","Retinol","레티놀","RETINOID",List.of("Vitamin A","레티놀"),List.of("RETINOID_SEARCH"),Map.of("WRINKLE_SCORE","CORE","ROUGH_SCORE","CORE"),Map.of("SOLUBILITY","OIL_SOLUBLE")),
i("00000000-0000-0000-0000-000000000106","Retinal","Retinal","레티날","RETINOID",List.of("Retinaldehyde","레티날"),List.of("RETINOID_SEARCH"),Map.of("WRINKLE_SCORE","CORE","ROUGH_SCORE","CORE"),Map.of()),
i("00000000-0000-0000-0000-000000000107","Niacinamide","Niacinamide","나이아신아마이드","VITAMIN_B3_DERIVATIVE",List.of("Nicotinamide","나이아신아마이드"),List.of("BRIGHTENING_SEARCH"),Map.of("MELANIN_SCORE","CORE","OILY_INTENSITY_SCORE","SUPPORT"),Map.of("SOLUBILITY","WATER_SOLUBLE")),
i("00000000-0000-0000-0000-000000000108","Ascorbic Acid","Ascorbic Acid","비타민 C","VITAMIN_C_DERIVATIVE",List.of("Vitamin C","L-Ascorbic Acid"),List.of("BRIGHTENING_SEARCH"),Map.of("MELANIN_SCORE","CORE","ROUGH_SCORE","SUPPORT"),Map.of("STABILITY","OXIDATION_SENSITIVE")),
i("00000000-0000-0000-0000-000000000109","Collagen","Collagen","콜라겐","PROTEIN",List.of("Hydrolyzed Collagen","콜라겐"),List.of("BARRIER_SEARCH"),Map.of("ROUGH_SCORE","SUPPORT"),Map.of()),
i("00000000-0000-0000-0000-000000000110","Madecassoside","Madecassoside","마데카소사이드","TRITERPENOID_DERIVATIVE",List.of("마데카소사이드"),List.of("SOOTHING_SEARCH","BARRIER_SEARCH"),Map.of("SENSITIVITY_SCORE","SUPPORT"),Map.of()),
i("00000000-0000-0000-0000-000000000111","Ceramide NP","Ceramide NP","세라마이드 NP","CERAMIDE",List.of("Ceramide","세라마이드"),List.of("BARRIER_SEARCH"),Map.of("BARRIER_SCORE","CORE","WATER_SCORE","CORE","SENSITIVITY_SCORE","SUPPORT"),Map.of("SOLUBILITY","OIL_DISPERSIBLE")),
i("00000000-0000-0000-0000-000000000112","Salicylic Acid","Salicylic Acid","살리실산","BETA_HYDROXY_ACID",List.of("BHA","살리실산"),List.of("EXFOLIATION_SEARCH"),Map.of("BLACKHEAD_SCORE","CORE","ACNE_SCORE","CORE"),Map.of()),
i("00000000-0000-0000-0000-000000000113","Kojic Acid","Kojic Acid","코직산","PHENOLIC_COMPOUND",List.of("Kojic","코직산"),List.of("BRIGHTENING_SEARCH"),Map.of("MELANIN_SCORE","CORE"),Map.of("STABILITY","OXIDATION_SENSITIVE")),
i("00000000-0000-0000-0000-000000000114","Guaiazulene","Guaiazulene","구아이아줄렌","AZULENE_DERIVATIVE",List.of("Azulene","아줄렌"),List.of("SOOTHING_SEARCH"),Map.of("SENSITIVITY_SCORE","SUPPORT","RED_SPOT_SCORE","SUPPORT"),Map.of("SOLUBILITY","OIL_SOLUBLE")),
i("00000000-0000-0000-0000-000000000115","Human Oligopeptide-1","Human Oligopeptide-1","EGF","PEPTIDE_GROWTH_FACTOR",List.of("EGF","상피세포성장인자"),List.of("EGF_SEARCH"),Map.of("ROUGH_SCORE","SUPPORT","WRINKLE_SCORE","SUPPORT"),Map.of("STABILITY","PROTEIN_STABILITY_SENSITIVE"))
,i("00000000-0000-0000-0000-000000000117","Sodium Hyaluronate","Sodium Hyaluronate","히알루론산 나트륨","HYALURONAN",List.of("히알루론산 나트륨"),List.of(),Map.of("WATER_SCORE","CORE","WRINKLE_SCORE","SUPPORT"),Map.of("SOLUBILITY","WATER_SOLUBLE"))
,i("00000000-0000-0000-0000-000000000118","Hydrolyzed Hyaluronic Acid","Hydrolyzed Hyaluronic Acid","가수분해 히알루론산","HYALURONAN",List.of("Hydrolyzed HA","가수분해 히알루론산"),List.of(),Map.of("WATER_SCORE","CORE","WRINKLE_SCORE","SUPPORT"),Map.of("SOLUBILITY","WATER_SOLUBLE"))
).forEach(seed -> repository.save(seed.getId(), seed.getInciName(), seed.getDisplayNameKo(),
seed.getFamily(), seed.getAliases(), seed.getEffects(), seed.getProperties()));
};
}

private Ingredient i(String id, String name, String inci, String ko, String family,
List<String> aliases, List<String> groups, Map<String,String> effects, Map<String,String> properties) {
return new Ingredient(id, inci, ko, family, aliases, effects, properties);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.seoulection.admin.ingredient.infrastructure.document;

import java.util.List;
import java.util.Map;

public class Ingredient {
private String id;
private String inciName;
private String displayNameKo;
private String family;
private List<String> aliases;
private Map<String, String> effects;
private Map<String, String> properties;
private List<EvidenceView> evidences;
private List<EfficacyRangeView> efficacyRanges;

protected Ingredient() { }

public Ingredient(String id, String inciName, String displayNameKo,
String family, List<String> aliases,
Map<String, String> effects, Map<String, String> properties) {
this(id, inciName, displayNameKo, family, aliases, effects, properties, List.of(), List.of());
}

public Ingredient(String id, String inciName, String displayNameKo,
String family, List<String> aliases,
Map<String, String> effects, Map<String, String> properties,
List<EvidenceView> evidences, List<EfficacyRangeView> efficacyRanges) {
this.id = id; this.inciName = inciName; this.displayNameKo = displayNameKo; this.family = family; this.aliases = aliases;
this.effects = effects; this.properties = properties;
this.evidences = evidences; this.efficacyRanges = efficacyRanges;
}

public String getId() { return id; }
public String getInciName() { return inciName; }
public String getDisplayNameKo() { return displayNameKo; }
public String getFamily() { return family; }
public List<String> getAliases() { return aliases == null ? List.of() : aliases; }
public Map<String, String> getEffects() { return effects == null ? Map.of() : effects; }
public Map<String, String> getProperties() { return properties == null ? Map.of() : properties; }
public List<EvidenceView> getEvidences() { return evidences == null ? List.of() : evidences; }
public List<EfficacyRangeView> getEfficacyRanges() { return efficacyRanges == null ? List.of() : efficacyRanges; }

public record EvidenceView(Long id, String sourceType, String title, String url, boolean humanEvidence,
String evidenceLevel, String targetScore, boolean ingredientSpecific,
String productForm, String studyConcentration, String studyConcentrationUnit,
String summary) { }

public record EfficacyRangeView(String targetKey, String productType, String concentrationMin,
String concentrationMax, String concentrationUnit,
String onsetConcentration, String irritationConcentration,
Long evidenceId, String notes, String role, List<ConditionView> conditions) {
public EfficacyRangeView(String targetKey, String productType, String concentrationMin,
String concentrationMax, String concentrationUnit,
String onsetConcentration, String irritationConcentration,
Long evidenceId, String notes) {
this(targetKey, productType, concentrationMin, concentrationMax, concentrationUnit,
onsetConcentration, irritationConcentration, evidenceId, notes, "SUPPORT", List.of());
}
}

public record ConditionView(String targetKey, String parameterKey, String valueText,
String valueMin, String valueMax, String valueUnit,
String conditionMode, boolean interpolationAllowed) { }
}
Loading