diff --git a/.vscode/settings.json b/.vscode/settings.json index 0958725..b26eb03 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + // https://code.visualstudio.com/docs/java/java-debugging "java.configuration.updateBuildConfiguration": "automatic", "java.compile.nullAnalysis.mode": "automatic", "java.dependency.syncWithFolderExplorer": true, @@ -8,7 +9,9 @@ }, "java.completion.favoriteStaticMembers": [ "org.hamcrest.Matchers.*", + "org.junit.jupiter.api.*", "org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*", "org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" - ] + ], + "java.debug.settings.hotCodeReplace": "manual" // change to "auto" if you want to enable } diff --git a/back/README.md b/back/README.md index 9fd9e58..d19f4e7 100644 --- a/back/README.md +++ b/back/README.md @@ -5,20 +5,28 @@ - [`ktlint --format`](https://github.com/pinterest/ktlint?tab=readme-ov-file#quick-start) -com.example - ├── core - │ ├── domain1 - │ │ ├── subcontext1 - │ │ ├── subcontext2 - │ │ └── common - │ ├── domain2 - │ └── shared - │ └── metadata - ├── api - │ ├── controller - │ └── service - └── util - -TODO: refactor project structure - -TODO: add questions \ No newline at end of file +# 2. 표현식 Expression +1. [x] 숫자 연산자 우선순위 오해 OperationPriority + - [x] 이진 시프트 BinaryShift + - [x] 비트 연산자 BitwiseOperator +2. [x] 조건식의 괄호 누락 MissingParentheses + - [x] &&, ||의 우선순위 LogicalOperatorPrecedence + - [x] 조건 연산자와 덧셈 TernaryWithAddition + - [x] 조건 연산자와 null 검사 TernaryWithNullCheck +3. [ ] 덧셈이 아닌 결합으로 작동 StringConcatenation +4. [ ] 멀티라인 문자열 리터럴 MultilineStringLiteral +5. [ ] 단항 덧셈 연산자 UnaryPlusOperator +6. [ ] 조건 표현식의 묵시적 타입 변환 ImplicitTypeConversion + - [ ] 조건 표현식의 박싱된 숫자 BoxedNumberConditional + - [ ] 중첩 조건 표현식 NestedConditional +7. [ ] 비단락 논리 연산자 사용 NonShortCircuitOperator +8. [ ] &&와 || 혼용 MixedLogicalOperators +9. [ ] 잘못된 가변 인수 호출 VarArgsIssues + - [ ] 모호한 가변 인수 호출 AmbiguousVarArgs + - [ ] 배열과 컬렉션 혼용 ArrayCollectionMixup + - [ ] 가변 인수에 원시 배열 전달 PrimitiveArrayToVarArgs +10. [ ] 조건 연산자와 가변 인수 호출 TernaryWithVarArgs +11. [ ] 반환값 무시 IgnoredReturnValue +12. [ ] 새롭게 생성된 객체를 사용하지 않음 UnusedObjects +13. [ ] 잘못된 메서드를 참조하는 바인딩 IncorrectMethodBinding +14. [ ] 메서드 참조 시 잘못된 메서드 지정 WrongMethodReference \ No newline at end of file diff --git a/back/build.gradle.kts b/back/build.gradle.kts index 82a5986..692064e 100644 --- a/back/build.gradle.kts +++ b/back/build.gradle.kts @@ -41,9 +41,8 @@ dependencies { testCompileOnly("org.projectlombok:lombok") testAnnotationProcessor("org.projectlombok:lombok") - // Document RESTful services by combining hand-written with Asciidoctor - // and auto-generated snippets produced with Spring MVC Test - testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") + // SpringDoc OpenAPI UI for REST API documentation + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5") // Spring Data base dependency without any database-specific functionality implementation("org.springframework.data:spring-data-commons") diff --git a/back/src/main/java/com/example/mistakes/QuestionEntityBuilder.java b/back/src/main/java/com/example/mistakes/QuestionEntityBuilder.java new file mode 100644 index 0000000..30ab115 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/QuestionEntityBuilder.java @@ -0,0 +1,19 @@ +package com.example.mistakes; + +import com.example.mistakes.api.questions.QuestionEntity; +import java.util.List; +import java.util.stream.Collectors; + +public class QuestionEntityBuilder { + public QuestionEntity build(Class cls) { + return new QuestionEntity(cls.getCanonicalName()); + } + + public static QuestionEntity of(Class cls) { + return new QuestionEntity(cls.getCanonicalName()); + } + + public static Iterable of(Class... cls) { + return List.of(cls).stream().map(QuestionEntityBuilder::of).collect(Collectors.toList()); + } +} diff --git a/back/src/main/java/com/example/mistakes/api/hello/HelloDTO.java b/back/src/main/java/com/example/mistakes/api/hello/HelloDTO.java index 4f5cada..13cbccd 100644 --- a/back/src/main/java/com/example/mistakes/api/hello/HelloDTO.java +++ b/back/src/main/java/com/example/mistakes/api/hello/HelloDTO.java @@ -1,6 +1,6 @@ package com.example.mistakes.api.hello; -import com.example.mistakes.base.type.definition.Message; +import com.example.mistakes.base.type.Message; @Deprecated public record HelloDTO(String message) implements Message {} diff --git a/back/src/main/java/com/example/mistakes/api/questions/ChaptersController.java b/back/src/main/java/com/example/mistakes/api/questions/ChaptersController.java new file mode 100644 index 0000000..e798710 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/api/questions/ChaptersController.java @@ -0,0 +1,49 @@ +package com.example.mistakes.api.questions; + +import com.example.mistakes.base.type.ResponseMany; +import com.example.mistakes.service.QuestionService; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class ChaptersController { + @Autowired private final QuestionService service; + + public ChaptersController(QuestionService service) { + this.service = service; + } + + private ResponseEntity> _response(Iterable data) { + final var dto = new ResClassQuestion(data); + return ResponseEntity.ok().body(dto); + } + + @GetMapping("/{chapterName}") + public ResponseEntity> getAllMistakesInChapter( + @PathVariable String chapterName) { + final var data = this.service.findAllByChapterName(chapterName); + return _response(data); + } + + @GetMapping("/{chapterName}/{mistakeId}") + public ResponseEntity> getExamplesInMistake( + @PathVariable String chapterName, @PathVariable Integer mistakeId) { + final var data = this.service.findAllByMistakeId(mistakeId); + return _response(data); + } + + @GetMapping("/{chapterName}/{mistakeId}/{exampleId}") + public ResponseEntity> getOneExample( + @PathVariable String chapterName, + @PathVariable Integer mistakeId, + @PathVariable Integer exampleId) { + final var data = this.service.findOne(chapterName, mistakeId, exampleId); + return _response(List.of(data)); + } +} diff --git a/back/src/main/java/com/example/mistakes/api/questions/ExamplesController.java b/back/src/main/java/com/example/mistakes/api/questions/ExamplesController.java new file mode 100644 index 0000000..6909dc0 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/api/questions/ExamplesController.java @@ -0,0 +1,37 @@ +package com.example.mistakes.api.questions; + +import com.example.mistakes.base.type.ResponseMany; +import com.example.mistakes.service.QuestionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/examples") +public class ExamplesController { + @Autowired private final QuestionService service; + + public ExamplesController(QuestionService service) { + this.service = service; + } + + private ResponseEntity> _response(Iterable data) { + final var dto = new ResClassQuestion(data); + return ResponseEntity.ok().body(dto); + } + + @GetMapping("/") + public ResponseEntity> getAll() { + final var data = this.service.findAll(); + return _response(data); + } + + @GetMapping("/{id}") + public ResponseEntity> getOneById(@PathVariable("id") Integer id) { + final var data = this.service.findAllByMistakeId(id); + return _response(data); + } +} diff --git a/back/src/main/java/com/example/mistakes/api/questions/QuestionController.java b/back/src/main/java/com/example/mistakes/api/questions/QuestionController.java deleted file mode 100644 index 4c41563..0000000 --- a/back/src/main/java/com/example/mistakes/api/questions/QuestionController.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.example.mistakes.api.questions; - -import com.example.mistakes.base.type.definition.ResponseMany; -import com.example.mistakes.base.type.template.ResponseManyDTO; -import java.util.List; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/questions") -public class QuestionController { - - private final List data = - // TODO: get data from service - List.of( - new QuestionEntity("A"), - new QuestionEntity("B"), - new QuestionEntity("C"), - new QuestionEntity("D")); - - @GetMapping("/t1") - public ResponseEntity> t1() { - final var dto = new ResRecordQuestion(this.data, this.data.size()); - return ResponseEntity.ok().body(dto); - } - - @GetMapping("/t2") - public ResponseEntity> t2() { - final var dto = new ResClassQuestion(this.data); - return ResponseEntity.ok().body(dto); - } - - @GetMapping("/t3") - public ResponseEntity> t3() { - final var dto = ResClassOfQuestion.of(this.data); - return ResponseEntity.ok().body(dto); - } -} - -// Type 1: record -record ResRecordQuestion(List result, Integer length) - implements ResponseMany {} - -// Type 2: custom wrapper DTO -final class ResClassQuestion extends ResponseManyDTO { - ResClassQuestion(Iterable result) { - super(result); - } -} - -// Type 3: custom wrapper DTO with static of -final class ResClassOfQuestion { - - private static class innerImpl extends ResponseManyDTO { - innerImpl(Iterable result) { - super(result); - } - } - - static ResponseManyDTO of(Iterable result) { - return new innerImpl(result); - } -} diff --git a/back/src/main/java/com/example/mistakes/api/questions/QuestionEntity.java b/back/src/main/java/com/example/mistakes/api/questions/QuestionEntity.java index d01fd03..3125eaa 100644 --- a/back/src/main/java/com/example/mistakes/api/questions/QuestionEntity.java +++ b/back/src/main/java/com/example/mistakes/api/questions/QuestionEntity.java @@ -1,5 +1,149 @@ package com.example.mistakes.api.questions; -import com.example.mistakes.base.type.definition.Message; +import com.example.mistakes.base.template.ResponseManyDTO; +import com.example.mistakes.base.type.FsMeta; +import com.example.mistakes.base.type.Identifiable; +import com.example.mistakes.base.type.Message; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.log4j.Log4j2; -public record QuestionEntity(String message) implements Message {} +final class ResClassQuestion extends ResponseManyDTO { + ResClassQuestion(Iterable result) { + super(result); + } +} + +// TODO: refactor to class +@Log4j2 +public record QuestionEntity(String message) implements Message, Identifiable, FsMeta { + + + private static Map chapterMap = new HashMap<>(Map.of("expression", 2)); + public Integer getChapter() { + var topLevelPackage = message.split("\\.")[3]; + return chapterMap.getOrDefault(topLevelPackage, 0); + } + + public String getId() { + var topLevelPackage = message.split("\\.")[3]; + var chapter = chapterMap.getOrDefault(topLevelPackage, 0); + + Pattern pattern = Pattern.compile("_(\\d+)_\\S+(\\d+)"); + Matcher matcher = pattern.matcher(message); + if (matcher.find()) { + String mistakeId = matcher.group(1); + String index = matcher.group(2); + return "%s_%s_%s".formatted(chapter, mistakeId, index); + } + + // legacy: return 1 when got `com/example/mistakes/expression/_01_OperationPriority.java` + String filtered = message.replaceAll("[^0-9]", ""); + if (filtered.isEmpty()) { + return String.valueOf(message.hashCode()); + } + return String.valueOf(Integer.parseInt(filtered)); + } + + public Path getPath() { + // return = src/main/java/com/example/mistakes/expression/_01_OperationPriority.java + // when message = com.example.mistakes.expression._01_OperationPriority.Ex2 + + String pathPrefix = "src/main/java"; + String fileExtension = ".java"; + + Pattern pattern = Pattern.compile("(\\S+_\\d+_[^.]+)"); + Matcher matcher = pattern.matcher(message); + + String path; + if (matcher.find()) { + String group = matcher.group(1); + path = group.replace(".", "/") + fileExtension; + } else { + throw new IllegalArgumentException("Matcher not found: message=" + message); + } + return Paths.get(pathPrefix, path); + } + + public String getClassName() { + // return = Ex2 + // when message = com.example.mistakes.expression._01_OperationPriority.Ex2 + + // temporary assume there is only one nested class + return message.substring(message.lastIndexOf(".") + 1); + } + + public String getBefore() { + return Code.readMethodCode(getPath(), getClassName(), "before"); + } + + public String getAfter() { + return Code.readMethodCode(getPath(), getClassName(), "after"); + } + + private class Code { + static String readMethodCode(Path filePath, String className, String methodName) { + try { + return read(filePath, className, methodName); + } catch (Exception e) { + return "NoSuchFileException path=%s methodName=%s" + .formatted(filePath, className, methodName); + } + } + + private static String read(Path filePath, String className, String methodName) + throws IOException { + + List classCode = findClassCode(filePath, className); + + StringBuilder methodCode = new StringBuilder(); + boolean methodFound = false; + for (String line : classCode) { + if (line.contains(" " + methodName + "(")) { // Simple check for method signature + methodFound = true; + methodCode.append(line).append("\n"); + } else if (methodFound && line.contains("}")) { + methodCode.append(line).append("\n"); + break; // Stop reading after the closing brace of the method + } else if (methodFound) { + methodCode.append(line).append("\n"); + } + } + return methodCode.toString(); + } + + private static List findClassCode(Path filePath, String className) throws IOException { + List classCode = new ArrayList<>(); + + boolean classFound = false; + int braceCount = 0; + for (String line : Files.readAllLines(filePath)) { + if (line.contains("class " + className)) { // Simple check for class signature + classFound = true; + classCode.add(line); + braceCount++; + } else if (classFound) { + classCode.add(line); + if (line.contains("{")) { + braceCount++; + } + if (line.contains("}")) { + braceCount--; + if (braceCount == 0) { + break; // Stop reading after the closing brace of the class + } + } + } + } + return classCode; + } + } +} diff --git a/back/src/main/java/com/example/mistakes/api/questions/service/QuestionService.java b/back/src/main/java/com/example/mistakes/api/questions/service/QuestionService.java deleted file mode 100644 index cc0c200..0000000 --- a/back/src/main/java/com/example/mistakes/api/questions/service/QuestionService.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.mistakes.api.questions.service; - -interface QuestionService { - - // 1. query by question id - T findById(ID id); - - // 2. chapter - Iterable findByChapter(String chapter); - - Iterable findByChapter(ID id); - - // 3. keywords - enum ConditionType { - AND, - OR, - } - - Iterable findByKeywords(String keyword); - - Iterable findByKeywords(Iterable keywords, ConditionType type); -} diff --git a/back/src/main/java/com/example/mistakes/api/questions/service/QuestionServiceImpl.java b/back/src/main/java/com/example/mistakes/api/questions/service/QuestionServiceImpl.java deleted file mode 100644 index 0729598..0000000 --- a/back/src/main/java/com/example/mistakes/api/questions/service/QuestionServiceImpl.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.mistakes.api.questions.service; - -import com.example.mistakes.api.questions.QuestionEntity; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class QuestionServiceImpl implements QuestionService { - - @Autowired private final ReadOnlyFsRepository repository; - - @Override - public QuestionEntity findById(Long id) { - return repository.findById(id).orElse(null); - } - - @Override - public Iterable findByChapter(String chapter) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public Iterable findByChapter(Long id) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public Iterable findByKeywords(String keyword) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public Iterable findByKeywords(Iterable keywords, ConditionType type) { - throw new UnsupportedOperationException("Not implemented"); - } -} diff --git a/back/src/main/java/com/example/mistakes/api/questions/service/ReadOnlyFsRepository.java b/back/src/main/java/com/example/mistakes/api/questions/service/ReadOnlyFsRepository.java deleted file mode 100644 index 333246b..0000000 --- a/back/src/main/java/com/example/mistakes/api/questions/service/ReadOnlyFsRepository.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.example.mistakes.api.questions.service; - -import java.util.Optional; -import java.util.function.Function; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; -import org.springframework.data.repository.query.QueryByExampleExecutor; -import org.springframework.stereotype.Repository; - -/** - * ReadOnlyFsRepository - * - *

Repository interface for read-only operations - * - * @param Entity - * @param Entity ID type - * @see org.springframework.data.repository.Repository - */ -@Repository -class ReadOnlyFsRepository implements CrudRepository, QueryByExampleExecutor { - - @Override - public long count() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void delete(T entity) { - // TODO Auto-generated method stub - - } - - @Override - public void deleteAll() { - // TODO Auto-generated method stub - - } - - @Override - public void deleteAll(Iterable entities) { - // TODO Auto-generated method stub - - } - - @Override - public void deleteAllById(Iterable ids) { - // TODO Auto-generated method stub - - } - - @Override - public void deleteById(ID id) { - // TODO Auto-generated method stub - - } - - @Override - public boolean existsById(ID id) { - // TODO Auto-generated method stub - return false; - } - - @Override - public Iterable findAll() { - // TODO Auto-generated method stub - return null; - } - - @Override - public Iterable findAllById(Iterable ids) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Optional findById(ID id) { - // TODO Auto-generated method stub - return Optional.empty(); - } - - @Override - public S save(S entity) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Iterable saveAll(Iterable entities) { - // TODO Auto-generated method stub - return null; - } - - @Override - public long count(Example example) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public boolean exists(Example example) { - // TODO Auto-generated method stub - return false; - } - - @Override - public Iterable findAll(Example example) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Iterable findAll(Example example, Sort sort) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Page findAll(Example example, Pageable pageable) { - // TODO Auto-generated method stub - return null; - } - - @Override - public R findBy( - Example example, Function, R> queryFunction) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Optional findOne(Example example) { - // TODO Auto-generated method stub - return Optional.empty(); - } -} diff --git a/back/src/main/java/com/example/mistakes/base/type/template/ResponseManyDTO.java b/back/src/main/java/com/example/mistakes/base/template/ResponseManyDTO.java similarity index 77% rename from back/src/main/java/com/example/mistakes/base/type/template/ResponseManyDTO.java rename to back/src/main/java/com/example/mistakes/base/template/ResponseManyDTO.java index 391f8cc..4796e68 100644 --- a/back/src/main/java/com/example/mistakes/base/type/template/ResponseManyDTO.java +++ b/back/src/main/java/com/example/mistakes/base/template/ResponseManyDTO.java @@ -1,11 +1,12 @@ -package com.example.mistakes.base.type.template; +package com.example.mistakes.base.template; -import com.example.mistakes.base.type.definition.ResponseMany; +import com.example.mistakes.base.type.ResponseMany; import java.util.List; import lombok.Getter; /** - * Wrapper class for implementing `ResponseMany` interface while supporting compatability with record-based DTOs + * Wrapper class for implementing `ResponseMany` interface while supporting compatability with + * record-based DTOs * * @param type of result * @see ResponseMany diff --git a/back/src/main/java/com/example/mistakes/base/type/FsMeta.java b/back/src/main/java/com/example/mistakes/base/type/FsMeta.java new file mode 100644 index 0000000..cb98c09 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/base/type/FsMeta.java @@ -0,0 +1,7 @@ +package com.example.mistakes.base.type; + +import java.nio.file.Path; + +public interface FsMeta { + Path getPath(); +} diff --git a/back/src/main/java/com/example/mistakes/base/type/Identifiable.java b/back/src/main/java/com/example/mistakes/base/type/Identifiable.java new file mode 100644 index 0000000..b0daa51 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/base/type/Identifiable.java @@ -0,0 +1,5 @@ +package com.example.mistakes.base.type; + +public interface Identifiable { + T getId(); +} diff --git a/back/src/main/java/com/example/mistakes/base/type/Message.java b/back/src/main/java/com/example/mistakes/base/type/Message.java new file mode 100644 index 0000000..d07d62b --- /dev/null +++ b/back/src/main/java/com/example/mistakes/base/type/Message.java @@ -0,0 +1,5 @@ +package com.example.mistakes.base.type; + +public interface Message { + String message(); +} diff --git a/back/src/main/java/com/example/mistakes/base/type/Response.java b/back/src/main/java/com/example/mistakes/base/type/Response.java new file mode 100644 index 0000000..1a29588 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/base/type/Response.java @@ -0,0 +1,5 @@ +package com.example.mistakes.base.type; + +public interface Response { + T result(); +} diff --git a/back/src/main/java/com/example/mistakes/base/type/definition/ResponseMany.java b/back/src/main/java/com/example/mistakes/base/type/ResponseMany.java similarity index 61% rename from back/src/main/java/com/example/mistakes/base/type/definition/ResponseMany.java rename to back/src/main/java/com/example/mistakes/base/type/ResponseMany.java index b45bb9e..b37ec74 100644 --- a/back/src/main/java/com/example/mistakes/base/type/definition/ResponseMany.java +++ b/back/src/main/java/com/example/mistakes/base/type/ResponseMany.java @@ -1,4 +1,4 @@ -package com.example.mistakes.base.type.definition; +package com.example.mistakes.base.type; public interface ResponseMany { Iterable result(); diff --git a/back/src/main/java/com/example/mistakes/base/type/definition/Message.java b/back/src/main/java/com/example/mistakes/base/type/definition/Message.java deleted file mode 100644 index 0facc04..0000000 --- a/back/src/main/java/com/example/mistakes/base/type/definition/Message.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.mistakes.base.type.definition; - -public interface Message { - String message(); -} diff --git a/back/src/main/java/com/example/mistakes/base/type/definition/Response.java b/back/src/main/java/com/example/mistakes/base/type/definition/Response.java deleted file mode 100644 index 7acf6c5..0000000 --- a/back/src/main/java/com/example/mistakes/base/type/definition/Response.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.mistakes.base.type.definition; - -public interface Response { - T result(); -} diff --git a/back/src/main/java/com/example/mistakes/expression/_01_OperationPriority.java b/back/src/main/java/com/example/mistakes/expression/_01_OperationPriority.java new file mode 100644 index 0000000..4ba1721 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/expression/_01_OperationPriority.java @@ -0,0 +1,65 @@ +package com.example.mistakes.expression; + +import com.example.mistakes.QuestionEntityBuilder; +import com.example.mistakes.service.QuestionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class _01_OperationPriority { + + _01_OperationPriority(@Autowired QuestionService service) { + var entities = QuestionEntityBuilder.of(Ex1.class, Ex2.class, Ex3.class, Ex4.class); + service.addAll(entities); + } + + static class Ex1 { + String subContext = "Binary Shift"; + + int before(short lo, short hi) { + return lo << 16 + hi; + } + + int after(short lo, short hi) { + return (lo << 16) + hi; + } + } + + static class Ex2 { + String subContext = "Binary Shift"; + short xmin = 1, ymin = 1, xmax = 1, ymax = 1; + + int before() { + return xmin + ymin << 8 + xmax << 16 + ymax << 24; + } + + int after() { + return xmin + (ymin << 8) + (xmax << 16) + (ymax << 24); + } + } + + static class Ex3 { + String subContext = "Binary Shift"; + int BLOCK_SIZE = 64 * 1024; + + int before() { + return BLOCK_SIZE + BLOCK_SIZE >> 2; + } + + int after() { + return BLOCK_SIZE + (BLOCK_SIZE >> 2); + } + } + + static class Ex4 { + String subContext = "Bitwise Operator"; + + int before(int bits) { + return bits & 0xFF00 + 1; + } + + int after(int bits) { + return (bits & 0xFF00) | 1; + } + } +} diff --git a/back/src/main/java/com/example/mistakes/expression/_02_MissingParentheses.java b/back/src/main/java/com/example/mistakes/expression/_02_MissingParentheses.java new file mode 100644 index 0000000..858f2fe --- /dev/null +++ b/back/src/main/java/com/example/mistakes/expression/_02_MissingParentheses.java @@ -0,0 +1,69 @@ +package com.example.mistakes.expression; + +import static java.util.Objects.requireNonNullElse; + +import com.example.mistakes.QuestionEntityBuilder; +import com.example.mistakes.service.QuestionService; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class _02_MissingParentheses { + + _02_MissingParentheses(@Autowired QuestionService service) { + var entities = QuestionEntityBuilder.of(Ex1.class, Ex2.class, Ex3.class, Ex4.class); + service.addAll(entities); + } + + static class Ex1 { + String subContext = "Logical operator precedence"; + + Boolean before(int index, String str) { + return index >= 0 && str.charAt(index) == ' ' || str.charAt(index) == '\t'; + } + + Boolean after(int index, String str) { + return index >= 0 && (str.charAt(index) == ' ' || str.charAt(index) == '\t'); + } + } + + static class Ex2 { + String subContext = "Ternary & addition"; + + StringBuilder before(String str, int indent) { + int capacity = str.length() + indent < 0 ? 0 : indent; + return new StringBuilder(capacity); + } + + StringBuilder after(String str, int indent) { + int capacity = str.length() + Math.max(indent, 0); + return new StringBuilder(capacity); + } + } + + static class Ex3 { + String subContext = "Ternary & null check"; + + String before(String value) { + return "Value: " + value != null ? value : "(unknown)"; + } + + String after(String value) { + return "Value: " + requireNonNullElse(value, "(unknown)"); + } + } + + static class Ex4 { + String subContext = "Ternary & null check"; + + int before(List input, String newItem) { + return input.size() + newItem == null ? 0 : 1; + } + + int after(List input, String newItem) { + int additionalElements = newItem == null ? 0 : 1; + return input.size() + additionalElements; + } + } +} diff --git a/back/src/main/java/com/example/mistakes/service/QuestionService.java b/back/src/main/java/com/example/mistakes/service/QuestionService.java new file mode 100644 index 0000000..7f408a2 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/service/QuestionService.java @@ -0,0 +1,27 @@ +package com.example.mistakes.service; + +import com.example.mistakes.api.questions.QuestionEntity; +import java.util.List; + +public interface QuestionService { + // Register + void add(QuestionEntity entity); + + void addAll(Iterable entity); + + // find one + QuestionEntity findOne(String id); + + QuestionEntity findOne(String chapterName, Integer mistakeId, Integer exampleId); + + QuestionEntity findOne(Integer chapterNumber, Integer mistakeId, Integer exampleId); + + // find many + List findAll(); + + List findAllByChapterName(String name); + + List findAllByChapterNumber(Number number); + + List findAllByMistakeId(Number id); +} diff --git a/back/src/main/java/com/example/mistakes/service/QuestionServiceImpl.java b/back/src/main/java/com/example/mistakes/service/QuestionServiceImpl.java new file mode 100644 index 0000000..c3a0e1d --- /dev/null +++ b/back/src/main/java/com/example/mistakes/service/QuestionServiceImpl.java @@ -0,0 +1,80 @@ +package com.example.mistakes.service; + +import com.example.mistakes.api.questions.QuestionEntity; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class QuestionServiceImpl implements QuestionService { + + @Autowired private final ReadOnlyFsRepository repository; + + Map chpaterMap = new HashMap<>(Map.of("expression", 2)); + + private Integer _convertChapterNameToNumber(String name) throws NoSuchElementException { + if (!chpaterMap.containsKey(name)) { + throw new NoSuchElementException("Chapter not found: %s".formatted(name)); + } + return chpaterMap.get(name); + } + + @Override + public void add(QuestionEntity entity) { + repository.save(entity); + } + + @Override + public void addAll(Iterable entity) { + repository.saveAll(entity); + } + + @Override + public QuestionEntity findOne(String id) { + return repository.findById(id).orElseThrow(); + } + + @Override + public QuestionEntity findOne(String chapterName, Integer mistakeId, Integer exampleId) { + var chapterNumber = _convertChapterNameToNumber(chapterName); + return findOne(chapterNumber, mistakeId, exampleId); + } + + @Override + public QuestionEntity findOne(Integer chapterNumber, Integer mistakeId, Integer exampleId) { + var id = "%d_%02d_%d".formatted(chapterNumber, mistakeId, exampleId); + return findOne(id); + } + + @Override + public List findAll() { + return (List) repository.findAll(); + } + + @Override + public List findAllByChapterName(String name) { + var chapterNumber = _convertChapterNameToNumber(name); + return findAllByChapterNumber(chapterNumber); + } + + @Override + public List findAllByChapterNumber(Number number) { + var regex = "%d_\\d+_\\d+".formatted(number); + return _findAllByPattern(regex); + } + + @Override + public List findAllByMistakeId(Number id) { + var regex = "\\d+_%02d_\\d+".formatted(id); + return _findAllByPattern(regex); + } + + private List _findAllByPattern(String regex) { + return (List) repository.findAllByPattern(regex); + } +} diff --git a/back/src/main/java/com/example/mistakes/service/ReadOnlyFsRepository.java b/back/src/main/java/com/example/mistakes/service/ReadOnlyFsRepository.java new file mode 100644 index 0000000..ab29034 --- /dev/null +++ b/back/src/main/java/com/example/mistakes/service/ReadOnlyFsRepository.java @@ -0,0 +1,110 @@ +package com.example.mistakes.service; + +import com.example.mistakes.base.type.FsMeta; +import com.example.mistakes.base.type.Identifiable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.springframework.data.repository.CrudRepository; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Repository; + +/** + * ReadOnlyFsRepository + * + *

Repository interface for read-only operations + * + * @param Entity + * @see org.springframework.data.repository.Repository + */ +@Repository +class ReadOnlyFsRepository & FsMeta, ID> + implements CrudRepository { + + Map data = new HashMap<>(); + + @Override + public @NonNull S save(@NonNull S entity) { + data.put(entity.getId(), entity); + return entity; + } + + @Override + public @NonNull Iterable saveAll(@NonNull Iterable entities) { + // use parallelStream() if entities came as a Collection and have many elements + entities.forEach(this::save); + return entities; + } + + @Override + public @NonNull Optional findById(@NonNull ID id) { + return Optional.ofNullable(data.get(id)); + } + + @Override + public @NonNull Iterable findAll() { + return List.copyOf(data.values()); + } + + @Override + public @NonNull Iterable findAllById(@NonNull Iterable ids) { + return StreamSupport.stream(ids.spliterator(), true) + .map(id -> data.get(id)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public @NonNull Iterable findAllByPattern(@NonNull String regex) { + Pattern pattern = Pattern.compile(regex); + return data.values().parallelStream() + .filter(entity -> pattern.matcher(entity.getId().toString()).matches()) + .collect(Collectors.toList()); + } + + @Override + public boolean existsById(ID id) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'existsById'"); + } + + @Override + public long count() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'count'"); + } + + @Override + public void deleteById(ID id) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'deleteById'"); + } + + @Override + public void delete(T entity) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'delete'"); + } + + @Override + public void deleteAllById(Iterable ids) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'deleteAllById'"); + } + + @Override + public void deleteAll(Iterable entities) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'deleteAll'"); + } + + @Override + public void deleteAll() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'deleteAll'"); + } +} diff --git a/back/src/main/resources/application.yaml b/back/src/main/resources/application.yaml index b1084f1..2cb627f 100644 --- a/back/src/main/resources/application.yaml +++ b/back/src/main/resources/application.yaml @@ -1,3 +1,7 @@ spring: application: name: mistakes + +springdoc: + swagger-ui: + use-root-path: true diff --git a/back/src/test/java/com/example/mistakes/api/BaseAPITests.java b/back/src/test/java/com/example/mistakes/api/BaseAPITests.java index 815178d..8f39f93 100644 --- a/back/src/test/java/com/example/mistakes/api/BaseAPITests.java +++ b/back/src/test/java/com/example/mistakes/api/BaseAPITests.java @@ -1,6 +1,7 @@ package com.example.mistakes.api; import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -8,6 +9,7 @@ import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Function; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -28,53 +30,17 @@ @SpringBootTest @AutoConfigureMockMvc @Slf4j -public class BaseAPITests { +class BaseAPITests { @Autowired private MockMvc mockMvc; @Autowired private RequestMappingHandlerMapping handlerMapping; - private ResultMatcher[] matchers() { - return new ResultMatcher[] { - status().isOk(), - content().contentType(MediaType.APPLICATION_JSON), - jsonPath("$.result").exists(), - jsonPath("$.length").value(greaterThan(0)) - }; - } - - @Test - public void testOneStatic() throws Exception { - mockMvc.perform(get("/api/questions/t1")).andExpectAll(matchers()); - } - - @Test - public void testSomeStatic() throws Exception { - Iterable apis = - List.of( - // "/api/hello", - "/api/questions/t1", "/api/questions/t2"); - for (String api : apis) { - mockMvc.perform(get(api)).andExpectAll(matchers()); - } - } - - @TestFactory - // testSingleDynamic() - public Stream testSomeStaticAsFactory() { - var urls = List.of("/api/questions/t1", "/api/questions/t2"); - return urls.parallelStream() - .map( - url -> - DynamicTest.dynamicTest( - url, () -> mockMvc.perform(get(url)).andExpectAll(matchers()))); - } - /** * Extract `GET /api/**` endpoints from handlerMapping * * @return List of endpoints */ - private List getEndpoints() { + List getEndpoints() { var keys = handlerMapping.getHandlerMethods().keySet().stream().toList(); @@ -104,17 +70,50 @@ private List getEndpoints() { return endpoints; } + @Test + void testEndPoints() { + List endpoints = + List.of( + "/error", + // swagger + "/v3/api-docs.yaml", + "/v3/api-docs", + "/v3/api-docs/swagger-config", + "/swagger-ui.html", + // examples + "/api/examples/", + "/api/examples/{id}", + // chapters + "/api/{chapterName}/{mistakeId}/{exampleId}", + "/api/{chapterName}", + "/api/{chapterName}/{mistakeId}", + // legacy + "/api/hello"); + + assertEquals(Set.copyOf(endpoints), Set.copyOf(getEndpoints())); + } + @TestFactory - public Stream testSomeDynamicAsFactory() { - var urls = List.of("/api/questions/t1", "/api/questions/t2"); - var endpoints = getEndpoints(); - assert endpoints.containsAll(urls); + Stream testSomeDynamicAsFactory() { + var matchers = + new ResultMatcher[] { + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.result").exists(), + jsonPath("$.length").value(greaterThan(0)) + }; return getEndpoints().stream() - .filter(e -> e.startsWith("/api/questions")) + .filter(e -> e.startsWith("/api") && !e.equals("/api/hello")) .map( - url -> - DynamicTest.dynamicTest( - url, () -> mockMvc.perform(get(url)).andExpectAll(matchers()))); + url -> { + String requestUrl = + url.replace("examples/{id}", "examples/2") + .replace("{chapterName}", "expression") + .replace("{mistakeId}", "2") + .replace("{exampleId}", "2"); + return DynamicTest.dynamicTest( + url, () -> mockMvc.perform(get(requestUrl)).andExpectAll(matchers)); + }); } } diff --git a/back/src/test/java/com/example/mistakes/api/hello/HelloAPITests.java b/back/src/test/java/com/example/mistakes/api/hello/HelloAPITests.java index 3f108cc..b6623d5 100644 --- a/back/src/test/java/com/example/mistakes/api/hello/HelloAPITests.java +++ b/back/src/test/java/com/example/mistakes/api/hello/HelloAPITests.java @@ -14,12 +14,12 @@ @SpringBootTest @AutoConfigureMockMvc -public class HelloAPITests { +class HelloAPITests { @Autowired private MockMvc mockMvc; @Test - public void testHelloEndpoint() throws Exception { + void testHelloEndpoint() throws Exception { mockMvc .perform(get("/api/hello")) .andExpect(status().isOk()) diff --git a/back/src/test/java/com/example/mistakes/api/questions/QuestionAPITests.java b/back/src/test/java/com/example/mistakes/api/questions/QuestionAPITests.java index 84de3eb..e8971fb 100644 --- a/back/src/test/java/com/example/mistakes/api/questions/QuestionAPITests.java +++ b/back/src/test/java/com/example/mistakes/api/questions/QuestionAPITests.java @@ -14,13 +14,13 @@ @SpringBootTest @AutoConfigureMockMvc -public class QuestionAPITests { +class QuestionAPITests { @Autowired private MockMvc mockMvc; - public void testGetQuestion() throws Exception { + void testGetQuestion() throws Exception { mockMvc - .perform(get("/api/questions/t1").accept(MediaType.APPLICATION_JSON)) + .perform(get("/api/questions/t3").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.result").exists()) diff --git a/back/src/test/java/com/example/mistakes/expression/_01_OperationPriorityTests.java b/back/src/test/java/com/example/mistakes/expression/_01_OperationPriorityTests.java new file mode 100644 index 0000000..4b81ee4 --- /dev/null +++ b/back/src/test/java/com/example/mistakes/expression/_01_OperationPriorityTests.java @@ -0,0 +1,128 @@ +package com.example.mistakes.expression; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.example.mistakes.service.QuestionService; +import java.util.List; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@RequiredArgsConstructor +class _01_OperationPriorityTests { + + @Autowired private QuestionService service; + + // need post-processing to remove redundant spaces + // i.g. double-spaces or tab or newline to single space + private String normalize(String code) { + return code.replaceAll("\\s+", " "); + } + + // TODO: move to BaseAPITests + @Test + void testRegistration() { + var message = "com.example.mistakes.expression._01_OperationPriority.Ex1"; + var classPath = "com/example/mistakes/expression/_01_OperationPriority.java"; + var before = "int before(short lo, short hi) { return lo << 16 + hi; }"; + var after = "int after(short lo, short hi) { return (lo << 16) + hi; }"; + var entity = service.findOne("2_01_1"); + assertEquals(entity.getId(), "2_01_1"); + assertEquals(entity.message(), message); + assertEquals(entity.getPath().toString(), "src/main/java/" + classPath); + + assertTrue(normalize(entity.getBefore()).contains(before)); + assertTrue(normalize(entity.getAfter()).contains(after)); + } + + @Test + void testRegistration2() { + var message = "com.example.mistakes.expression._01_OperationPriority.Ex2"; + var classPath = "com/example/mistakes/expression/_01_OperationPriority.java"; + var before = "int before() { return xmin + ymin << 8 + xmax << 16 + ymax << 24; }"; + var after = "int after() { return xmin + (ymin << 8) + (xmax << 16) + (ymax << 24); }"; + + // var entity = service.findAll().get(1); + // System.out.println(entity); + // System.out.println(entity.message()); + // System.out.println(entity.getId()); + // System.out.println(entity.getPath().toString()); + // System.out.println(entity.getBefore()); + // System.out.println(entity.getAfter()); + + var entity = service.findOne("2_01_2"); + // var entity = service.find(2, 2); + assertEquals(entity.message(), message); + assertEquals(entity.getId(), "2_01_2"); + assertEquals(entity.getPath().toString(), "src/main/java/" + classPath); + + assertTrue(normalize(entity.getBefore()).contains(before)); + assertTrue(normalize(entity.getAfter()).contains(after)); + } + + @ParameterizedTest + @MethodSource("dualCombinations") + void testEx1(short lo, short hi) { + var target = new _01_OperationPriority.Ex1(); + int expected = lo * (int) Math.pow(2, 16) + hi; + assertEquals(expected, target.after(lo, hi), "Test failed with lo=%d hi=%d".formatted(lo, hi)); + } + + static Stream dualCombinations() { + var values = List.of((short) 0, (short) 1, (short) 2, Short.MAX_VALUE); + return values.stream().flatMap(lo -> values.stream().map(hi -> Arguments.of(lo, hi))); + } + + @Test + void testEx2() { + var target = new _01_OperationPriority.Ex2(); + + printBinary(1); + printBinary(1 + 1); + printBinary(1 + 1 << 8); + printBinary(1 + 1 << 8 + 1); + printBinary(1 + 1 << 8 + 1 << 16); + printBinary(1 + 1 << 8 + 1 << 16 + 1); + printBinary(1 + 1 << 8 + 1 << 16 + 1 << 24); + printBinary(target.after()); + + assertEquals(target.before(), 0); + } + + private static void printBinary(int value) { + printBinary(value, 32); + } + + private static void printBinary(int value, int length) { + System.out.println( + String.format("%" + length + "s", Integer.toBinaryString(value)).replace(' ', '0')); + } + + @Test + void testEx3() { + var target = new _01_OperationPriority.Ex3(); + + assertEquals(target.before(), target.BLOCK_SIZE * 0.5); + assertEquals(target.after(), target.BLOCK_SIZE * 1.25); + } + + @Test + void testEx4() { + var target = new _01_OperationPriority.Ex4(); + var input = 0x0FF0; + + printBinary(input, 16); + printBinary(target.before(input), 16); + printBinary(target.after(input), 16); + + assertEquals(target.before(input) % 2, input % 2); + assertEquals(target.after(input) % 2, 1); + } +} diff --git a/back/src/test/java/com/example/mistakes/expression/_02_MissingParenthesesTests.java b/back/src/test/java/com/example/mistakes/expression/_02_MissingParenthesesTests.java new file mode 100644 index 0000000..a8fd84c --- /dev/null +++ b/back/src/test/java/com/example/mistakes/expression/_02_MissingParenthesesTests.java @@ -0,0 +1,60 @@ +package com.example.mistakes.expression; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@RequiredArgsConstructor +class _02_MissingParenthesesTests { + + @Test + void testEx1() { + var target = new _02_MissingParentheses.Ex1(); + + assertThrows(StringIndexOutOfBoundsException.class, () -> target.before(-1, " ")); + assertDoesNotThrow(() -> target.after(-1, " ")); + + assertTrue(target.after(0, "\t")); + assertTrue(target.after(1, "a ")); + } + + @Test + void testEx2() { + var target = new _02_MissingParentheses.Ex2(); + + assertThrows(NegativeArraySizeException.class, () -> target.before("abc", -1)); + assertDoesNotThrow(() -> target.after("abc", -1)); + + assertTrue(target.after("abc", 0).capacity() == 3); + } + + @Test + void testEx3() { + var target = new _02_MissingParentheses.Ex3(); + + var whenNull = "Value: (unknown)"; + assertNotEquals(target.before(null), whenNull); + assertEquals(target.after(null), whenNull); + } + + @Test + void testEx4() { + var target = new _02_MissingParentheses.Ex4(); + + var init = List.of("a", "b", "c"); + + assertEquals(target.before(init, null), 1); + assertEquals(target.before(init, "d"), 1); + + assertEquals(target.after(init, null), 3); + assertEquals(target.after(init, "d"), 4); + } +} diff --git a/front/astro.config.mjs b/front/astro.config.mjs index 17f6a62..3cb8529 100644 --- a/front/astro.config.mjs +++ b/front/astro.config.mjs @@ -2,4 +2,8 @@ import { defineConfig } from "astro/config"; // https://astro.build/config -export default defineConfig({}); +export default defineConfig({ + site: "https://oomia.github.io/100_java_mistakes/", + base: "/100_java_mistakes", + trailingSlash: "ignore", +}); diff --git a/front/src/components/GlobalStyles.astro b/front/src/components/GlobalStyles.astro index 184be63..209b112 100644 --- a/front/src/components/GlobalStyles.astro +++ b/front/src/components/GlobalStyles.astro @@ -1,24 +1,27 @@ + + diff --git a/front/src/components/PageHandler.astro b/front/src/components/PageHandler.astro new file mode 100644 index 0000000..5278837 --- /dev/null +++ b/front/src/components/PageHandler.astro @@ -0,0 +1,83 @@ +--- +// 현재 URL을 가져와서 경로를 분석합니다 +const url = Astro.url; +const pathSegments = url.pathname.split("/").filter(Boolean); +const lastSegment = pathSegments[pathSegments.length - 1]; + +// 마지막 세그먼트가 숫자인지 확인 +const isNumeric = /^\d+$/.test(lastSegment); +const currentPage = isNumeric ? parseInt(lastSegment) : null; + +// 이전/다음 페이지 URL 생성 +let prevPageUrl, nextPageUrl; +if (isNumeric && currentPage !== null) { + const basePath = pathSegments.slice(0, -1).join("/"); + prevPageUrl = currentPage > 1 ? `/${basePath}/${currentPage - 1}` : null; + nextPageUrl = `/${basePath}/${currentPage + 1}`; +} + +// 내비게이션 버튼이 표시되어야 하는지 결정 +const showNavigation = isNumeric && currentPage !== null; +--- + +{ + showNavigation && ( +

+ ) +} + + diff --git a/front/src/components/core/Anchor.astro b/front/src/components/core/Anchor.astro new file mode 100644 index 0000000..5fe8fef --- /dev/null +++ b/front/src/components/core/Anchor.astro @@ -0,0 +1,42 @@ +--- +const { href, text } = Astro.props; +const baseUrl = import.meta.env.BASE_URL; +const extendedHref = `${baseUrl}${href}`.replaceAll("//", "/"); +--- + +{text} + + diff --git a/front/src/components/core/CodeCard.astro b/front/src/components/core/CodeCard.astro new file mode 100644 index 0000000..b83e9f9 --- /dev/null +++ b/front/src/components/core/CodeCard.astro @@ -0,0 +1,57 @@ +--- +import CodeWrapper from "./CodeWrapper.astro"; + +const { title, summary, code, lang } = Astro.props; +--- + +
+

{title}

+
{summary}
+ +
+ + diff --git a/front/src/components/core/CodeWrapper.astro b/front/src/components/core/CodeWrapper.astro new file mode 100644 index 0000000..0e4ce22 --- /dev/null +++ b/front/src/components/core/CodeWrapper.astro @@ -0,0 +1,51 @@ +--- +import { Code } from "astro:components"; + +const { code, lang } = Astro.props; + +const jsonify = (data: string) => JSON.stringify(data, null, 2); + +// 공통 들여쓰기 제거 +const trimIndents = (data: string) => { + // 줄 단위로 분리 + const lines = data.split("\n"); + + // 모든 줄의 공통 들여쓰기 최소값 찾기 (빈 줄은 무시) + let minIndent = Infinity; + for (const line of lines) { + // 공백만 있는 줄은 건너뛰기 + if (line.trim().length === 0) continue; + + // 앞쪽 공백 찾기 + const matched = line.match(/^(\s*)/); + if (!matched) continue; + + const indent = matched[1].length; + if (indent < minIndent) { + minIndent = indent; + } + } + + // 모든 줄이 빈 줄인 경우 예외 처리 + if (minIndent === Infinity) minIndent = 0; + + // 각 줄에서 공통 들여쓰기만 제거 + const trimmedLines = lines.map((line) => { + if (line.trim().length === 0) return line; + return line.substring(minIndent); + }); + + return trimmedLines.join("\n"); +}; + +const data = lang === "json" ? jsonify(code) : trimIndents(code); +--- + + diff --git a/front/src/components/core/Link.astro b/front/src/components/core/Link.astro new file mode 100644 index 0000000..275fdab --- /dev/null +++ b/front/src/components/core/Link.astro @@ -0,0 +1,7 @@ +--- +const { href, rel, type } = Astro.props; +const baseUrl = import.meta.env.BASE_URL; +const extendedHref = `${baseUrl}${href}`.replaceAll("//", "/"); +--- + + diff --git a/front/src/layouts/ApiDataLayout.astro b/front/src/layouts/ApiDataLayout.astro index b8b882c..7446cb3 100644 --- a/front/src/layouts/ApiDataLayout.astro +++ b/front/src/layouts/ApiDataLayout.astro @@ -1,6 +1,7 @@ --- +import CodeCard from "@/components/core/CodeCard.astro"; +import PageHandler from "@/components/PageHandler.astro"; import { fetchDataWithRetry } from "@/modules/fetchData"; -import { Code } from "astro:components"; export interface Props { apiPath: string; @@ -11,7 +12,7 @@ export interface Props { baseUrl?: string; } -const { +let { apiPath, title = "API Data Display", description = "Data fetched from API endpoint", @@ -22,12 +23,25 @@ const { console.log(apiPath); const { data } = await fetchDataWithRetry(apiPath); -let { message } = data; +// TODO: layout should applied for each result, not only the first one +const result = data.result ? data.result[0] : []; +const { message, before, after, id, path } = data.result![0]; + +// TODO: use search with regex +title = message + ? message + .split("/") + .pop() + ?.replace(".java", "") + .replace(/^_(\d+)_/, "#$1 ") || title + : title; ---
-

{message || title}

+

+ {title} +

@@ -36,25 +50,20 @@ let { message } = data;
-
-

API Response

-
GET /{apiPath}
- - -
- + + + + +
diff --git a/front/src/layouts/HelloLayout.astro b/front/src/layouts/HelloLayout.astro new file mode 100644 index 0000000..9608934 --- /dev/null +++ b/front/src/layouts/HelloLayout.astro @@ -0,0 +1,123 @@ +--- +import CodeCard from "@/components/core/CodeCard.astro"; +import { fetchDataWithRetry } from "@/modules/fetchData"; + +export interface Props { + apiPath: string; + title?: string; + description?: string; + maxRetries?: number; + delayMs?: number; + baseUrl?: string; +} + +const { + apiPath, + title = "API Data Display", + description = "Data fetched from API endpoint", + maxRetries = 3, + delayMs = 10000, + baseUrl = "http://localhost:8080", +} = Astro.props; + +console.log(apiPath); +const { data } = await fetchDataWithRetry(apiPath); +let { message } = data; +--- + +
+
+

{message || title}

+
+ +
+

🚀 {title}

+

{description}

+ +
+ + +
+ + diff --git a/front/src/layouts/Landing.astro b/front/src/layouts/Landing.astro index 491788e..b032e95 100644 --- a/front/src/layouts/Landing.astro +++ b/front/src/layouts/Landing.astro @@ -1,13 +1,20 @@ - +--- +import Link from "@/components/core/Link.astro"; +import Navigator from "@/components/Navigator.astro"; +--- + - + Astro Basics + diff --git a/front/src/modules/fetchData.ts b/front/src/modules/fetchData.ts index 8c8a0f4..54aabd9 100644 --- a/front/src/modules/fetchData.ts +++ b/front/src/modules/fetchData.ts @@ -1,10 +1,16 @@ interface Response { message?: string; - result?: any; + result?: { + id: number; + message: string; + before: string; + after: string; + path: string; + }[]; length?: number; } -export async function fetchWithRetry( +async function _fetchWithRetry( url: string, retriesLeft: number, maxRetries: number = 3, @@ -21,7 +27,7 @@ export async function fetchWithRetry( console.log(`Retrying... (${maxRetries - retriesLeft + 1}/${maxRetries})`); await new Promise((resolve) => setTimeout(resolve, delayMs)); - return fetchWithRetry(url, retriesLeft - 1); + return _fetchWithRetry(url, retriesLeft - 1); } } @@ -34,7 +40,9 @@ export async function fetchDataWithRetry( let data = { message: "No Data" }; let message = "No Data"; try { - data = await fetchWithRetry(`${baseUrl}/${apiPath}`, maxRetries, delayMs); + const url = `${baseUrl}/${apiPath}`; + console.log(`Fetching data from ${url}`); + data = await _fetchWithRetry(url, maxRetries, delayMs); message = data.message || "Data Loaded Successfully"; } catch (error) { if (error instanceof Error) { diff --git a/front/src/modules/getEndpoints.ts b/front/src/modules/getEndpoints.ts index e012e2b..e14d706 100644 --- a/front/src/modules/getEndpoints.ts +++ b/front/src/modules/getEndpoints.ts @@ -1,12 +1,45 @@ -export const endpoints: { subject: string; id: string }[] = [ - { - subject: "questions", - id: "t1", - }, - { - subject: "questions", - id: "t2", - }, -]; - -// TODO: modify to actual endpoints \ No newline at end of file +import { fetchDataWithRetry } from "./fetchData"; + +const mistakePerChapter = { + expression: 2, //14, + project_structure: 0, // 11, +}; + +export const urlList = Object.entries(mistakePerChapter) + .filter(([, mistakeNumber]) => mistakeNumber > 0) + .flatMap(([chapterName, mistakeNumber]) => { + return Array.from({ length: mistakeNumber }, (_, i) => { + return `${chapterName}/${i + 1}`; + }); + }); +console.log(`urlList:`, urlList); + +async function fetchEndpoint() { + const result: Endpoint[] = []; + for (const url of urlList) { + const data = await fetchDataWithRetry(`api/${url}`); + const chapterName = url.split("/")[0]; + const mistakeId = parseInt(url.split("/")[1]); + const len = data.data.length || 0; + + const a = Array.from({ length: len }, (_, i) => { + return { + chapterName: chapterName, + mistakeId: mistakeId, + exampleId: i + 1, + } as Endpoint; + }); + + result.push(...a); + } + return result; +} + +interface Endpoint { + chapterName: string; + mistakeId: number; + exampleId: number; +} + +export const endpoints: Endpoint[] = await fetchEndpoint(); +console.log(`endpoints:`, endpoints); diff --git a/front/src/pages/[subject]/[id].astro b/front/src/pages/[chapterName]/[mistakeId]/[exampleId].astro similarity index 54% rename from front/src/pages/[subject]/[id].astro rename to front/src/pages/[chapterName]/[mistakeId]/[exampleId].astro index 1070bcc..c88bcf7 100644 --- a/front/src/pages/[subject]/[id].astro +++ b/front/src/pages/[chapterName]/[mistakeId]/[exampleId].astro @@ -7,19 +7,26 @@ import { endpoints } from "@/modules/getEndpoints"; export function getStaticPaths() { return endpoints.map((e) => { return { - params: { subject: e.subject, id: e.id }, + params: { + chapterName: e.chapterName, + mistakeId: e.mistakeId, + exampleId: e.exampleId, + }, }; }); } + const prefix = "api"; -const { subject, id } = Astro.params; +const { chapterName, mistakeId, exampleId } = Astro.params; +const url = `${prefix}/${chapterName}/${mistakeId}/${exampleId}`; +console.log(url); --- diff --git a/front/src/pages/index.astro b/front/src/pages/index.astro index 481bd2a..0deb7c9 100644 --- a/front/src/pages/index.astro +++ b/front/src/pages/index.astro @@ -1,12 +1,12 @@ --- import GlobalStyles from "@/components/GlobalStyles.astro"; -import ApiDataLayout from "@/layouts/ApiDataLayout.astro"; +import HelloLayout from "@/layouts/HelloLayout.astro"; import Layout from "@/layouts/Landing.astro"; --- -