├── .gitignore ├── Dockerfile ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── millky │ │ └── blog │ │ ├── Booter.java │ │ ├── application │ │ ├── aop │ │ │ ├── CategoryInterceptor.java │ │ │ ├── UserSessionArgumentResolver.java │ │ │ └── UserSessionInterceptor.java │ │ ├── configuration │ │ │ ├── BlogConfig.java │ │ │ ├── OpenApiConfig.java │ │ │ ├── SecurityConfig.java │ │ │ ├── SwaggerConfig.java │ │ │ └── WebMvcConfig.java │ │ └── utility │ │ │ └── StringUtility.java │ │ ├── domain │ │ ├── model │ │ │ ├── UserSession.java │ │ │ ├── command │ │ │ │ └── PostCommand.java │ │ │ ├── entity │ │ │ │ ├── Category.java │ │ │ │ ├── Comment.java │ │ │ │ ├── Post.java │ │ │ │ ├── PostTag.java │ │ │ │ └── Tag.java │ │ │ └── exception │ │ │ │ └── IllegalUserException.java │ │ ├── repository │ │ │ ├── CategoryRepository.java │ │ │ ├── PostRepository.java │ │ │ ├── PostSearchRepository.java │ │ │ ├── PostTagRepository.java │ │ │ └── TagRepository.java │ │ └── service │ │ │ ├── PostSearchService.java │ │ │ ├── PostService.java │ │ │ ├── TagService.java │ │ │ ├── factory │ │ │ └── SearchServiceFactory.java │ │ │ └── impl │ │ │ ├── PostContentSearchService.java │ │ │ ├── PostTitleAndContentSearchService.java │ │ │ └── PostTitleSearchService.java │ │ ├── infrastructure │ │ └── dao │ │ │ ├── CategoryDao.java │ │ │ ├── CommentDao.java │ │ │ ├── PostDao.java │ │ │ ├── PostTagDao.java │ │ │ └── TagDao.java │ │ └── presentation │ │ ├── controller │ │ ├── rest │ │ │ ├── CommentRestController.java │ │ │ ├── TagRecord.java │ │ │ └── TagRestController.java │ │ └── web │ │ │ ├── CategoryController.java │ │ │ ├── HelloController.java │ │ │ ├── UserController.java │ │ │ └── post │ │ │ ├── PostDeleteController.java │ │ │ ├── PostEditController.java │ │ │ ├── PostSearchController.java │ │ │ ├── PostViewController.java │ │ │ └── PostWriteController.java │ │ └── support │ │ ├── GlobalExceptionHandler.java │ │ └── JsonBuilder.java ├── resources │ ├── application.yml │ ├── build │ │ └── build.gradle │ ├── messages.properties │ ├── messages_ko.properties │ ├── static │ │ ├── favicon.ico │ │ └── image │ │ │ ├── about-bg.jpg │ │ │ ├── contact-bg.jpg │ │ │ ├── home-bg.jpg │ │ │ ├── millky.png │ │ │ ├── post-bg.jpg │ │ │ └── top.png │ └── templates │ │ ├── hello.html │ │ └── hello.vm └── webapp │ └── WEB-INF │ ├── jsp │ ├── connect │ │ └── login.jsp │ ├── error.jsp │ ├── hello.jsp │ └── post │ │ ├── form.jsp │ │ ├── list.jsp │ │ ├── post.jsp │ │ └── rss.jsp │ └── jspf │ ├── footer.jspf │ ├── head.jspf │ └── nav.jspf └── test └── java └── com └── millky └── blog └── SpringBlogApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .mvn/ 2 | mvnw 3 | mvnw.cmd 4 | 5 | *.sw? 6 | .#* 7 | *# 8 | *~ 9 | 10 | .factorypath 11 | .springBeans 12 | 13 | interpolated*.xml 14 | dependency-reduced-pom.xml 15 | build.log 16 | _site/ 17 | .*.md.html 18 | manifest.yml 19 | MANIFEST.MF 20 | settings.xml 21 | activemq-data 22 | overridedb.* 23 | *.iml 24 | *.ipr 25 | *.iws 26 | .idea 27 | 28 | 29 | # Directories # 30 | /build/ 31 | /bin/ 32 | target/ 33 | /code 34 | 35 | .gradle 36 | 37 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 38 | hs_err_pid* 39 | 40 | 41 | # OS Files # 42 | .DS_Store 43 | 44 | *.class 45 | 46 | # Package Files # 47 | *.jar 48 | *.war 49 | *.ear 50 | *.db 51 | 52 | 53 | ###################### 54 | # Windows 55 | ###################### 56 | 57 | # Windows image file caches 58 | Thumbs.db 59 | 60 | # Folder config file 61 | Desktop.ini 62 | 63 | 64 | ###################### 65 | # OSX 66 | ###################### 67 | 68 | .DS_Store 69 | .svn 70 | 71 | # Thumbnails 72 | ._* 73 | 74 | # Files that might appear on external disk 75 | .Spotlight-V100 76 | .Trashes 77 | 78 | 79 | ###################### 80 | # Eclipse 81 | ###################### 82 | 83 | *.pydevproject 84 | .project 85 | .metadata 86 | bin/** 87 | tmp/** 88 | tmp/**/* 89 | *.tmp 90 | *.bak 91 | *.swp 92 | *~.nib 93 | local.properties 94 | .classpath 95 | .settings/ 96 | .loadpath 97 | /src/main/resources/rebel.xml 98 | # External tool builders 99 | .externalToolBuilders/ 100 | 101 | # Locally stored "Eclipse launch configurations" 102 | *.launch 103 | 104 | # CDT-specific 105 | .cproject 106 | 107 | # PDT-specific 108 | .buildpath 109 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.6-openjdk-8 2 | 3 | ARG ARG_PROFILES 4 | 5 | ENV SPRING_PROFILES_ACTIVE prod 6 | 7 | ENV APP_ID spring-blog 8 | ENV APP_DIR /millky/service/${APP_ID} 9 | 10 | WORKDIR $APP_DIR 11 | COPY . $APP_DIR 12 | RUN ["mvn", "clean", "package", "-DskipTests=true"] 13 | 14 | ENV JAVA_DEFAULT_CFG -jar -server -Dsun.net.inetaddr.ttl=0 -Djava.security.egd=file:/dev/./urandom -Dfile.encoding=UTF-8 -Duser.timezone=GMT+09:00 15 | ENV JAVA_MEM -Xms1G -Xmx2G 16 | 17 | EXPOSE 8080 18 | 19 | CMD java -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} ${JAVA_DEFAULT_CFG} ${JAVA_MEM} target/${APP_ID}.jar 20 | 21 | ## docker build -t spring-blog . 22 | ## docker tag spring-blog:latest 192.168.50.100:5050/spring-blog 23 | ## docker push 192.168.50.100:5050/spring-blog -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringBlog from Millky 2 | ### SpringBoot 3 base Open-source Blog 3 | 4 | 밀키(millky.com)에서 블로그 부분을 뽑아(?) 오픈소스 합니다. 5 | 6 | ``` 7 | SpringBoot3 + Spring Data JPA + Spring Security + ... 8 | ``` 9 | 10 | 단지 코드만 공개한는 것이 아니라. 개발 과정을 같이 공개하려 합니다. 11 | 12 | ### LIVE DEMO 13 | - http://blog.millky.com/post/list 14 | - 제공하는 API를 확인해 보세요~ http://blog.millky.com/swagger-ui/index.html#/tag-rest-controller/tagCloud 15 | 16 | ### 개발된 기능 17 | - 기본적인 블로그 뷰 18 | - 페이스북, 구글 소셜 로그인(깃헙등도 간단하게 추가 가능) 19 | - 글쓰기, 수정, 삭제. 페이징 20 | - 댓글쓰기, 삭제 21 | - 카테고리 22 | - 태그 23 | - RSS 뷰 24 | - 간단한 검색(DB) 25 | 26 | #### 추가 예정 27 | - 추가적인 소셜 로그인 및 기본 로그인 28 | - 댓글 수정 29 | - 국제화(i18n 다국어 지원) 30 | - 기타 DB 지원 31 | - 간단한 캐싱 (EHCache ?) 32 | 33 | 34 | ### Quick Start 35 | 설치되어 있어야 할 것 36 | - JDK 17 이상 37 | - Maven 38 | - Git 39 | 40 | ``` 41 | git clone https://github.com/origoni/Spring-Blog 42 | cd Spring-Blog 43 | mvn spring-boot:run 44 | ``` 45 | 46 | - visit [http://localhost:8080/](http://localhost:8080/) 47 | 48 | 49 | ### Tested 50 | - STS(Eclipse) ?? 51 | - IntelliJ IDEA 2022.3 52 | 53 | ``` 54 | //@formatter:off & //@formatter:on 55 | eclipse : Preferences -> Java -> Code style -> Formatter -> Edit... (or New...) > Off/On Tags 56 | intellij : Preferences -> Editor -> Code Style > Formatter Control > Enable formatter markers in comments 57 | ``` 58 | 59 | 60 | ### 관련 링크 61 | - http://millky.com/ 62 | - http://millky.com/@origoni 63 | - http://millky.com/@origoni/folder/30/post/list 64 | 65 | ### Project Convention 66 | 67 | #### Package Structure 68 | 69 | ``` 70 | com.millky.blog 71 | └── application 72 | └── aop 73 | └── configuration 74 | └── utility 75 | └── domain 76 | └── model 77 | └── command 78 | └── entity 79 | └── exception 80 | └── repository 81 | └── service 82 | └── infrastructure 83 | └── dao 84 | └── presentation 85 | └── controller 86 | └── rest 87 | └── web 88 | └── support 89 | └── result 90 | ``` 91 | 92 | #### Environment 93 | - Java version: 17 94 | - Spring Boot version: 3.0.1 95 | - Default Encoding: UTF-8 96 | - Default SCM : git 97 | 98 | #### 프로젝트 설정 99 | 1. STS 설치 -> http://millky.com/@origoni/post/1100 100 | 2. Lombok 설치 -> https://projectlombok.org/ (STS에 설치 : http://millky.com/@origoni/post/1164) 101 | 3. GitHub 다운 -> http://millky.com/@origoni/post/1145 (OSX : http://millky.com/@origoni/post/1140) 102 | 103 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.millky.blog 7 | spring-blog 8 | 0.0.25-SNAPSHOT 9 | jar 10 | 11 | SpringBlog 12 | SpringBlog project from Millky 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 3.0.1 18 | 19 | 20 | 21 | 22 | UTF-8 23 | com.millky.blog.Booter 24 | 17 25 | / 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-data-jpa 32 | 33 | 34 | com.h2database 35 | h2 36 | 37 | 38 | com.zaxxer 39 | HikariCP 40 | 41 | 42 | 43 | org.springframework.security 44 | spring-security-oauth2-client 45 | 46 | 47 | org.springframework.security 48 | spring-security-oauth2-jose 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-security 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-web 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | jakarta.servlet.jsp.jstl 69 | jakarta.servlet.jsp.jstl-api 70 | 3.0.0 71 | 72 | 73 | 74 | 75 | 76 | org.glassfish.web 77 | jakarta.servlet.jsp.jstl 78 | 79 | 3.0.1 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-starter-validation 96 | 97 | 98 | 99 | org.apache.tomcat.embed 100 | tomcat-embed-jasper 101 | provided 102 | 103 | 104 | org.springframework.boot 105 | spring-boot-starter-tomcat 106 | provided 107 | 108 | 109 | 110 | org.webjars 111 | webjars-locator-core 112 | 113 | 114 | org.webjars.bower 115 | bootstrap 116 | 3.3.7 117 | 118 | 119 | org.webjars.bower 120 | font-awesome 121 | 4.7.0 122 | 123 | 124 | org.webjars.bower 125 | jquery 126 | 3.2.1 127 | 128 | 129 | org.webjars.bower 130 | adminlte 131 | 2.3.11 132 | 133 | 134 | org.webjars.bower 135 | origoni-startbootstrap-clean-blog 136 | 1.0.3 137 | 138 | 139 | 140 | 141 | 142 | org.webjars.bower 143 | pen 144 | 0.2.2 145 | 146 | 147 | 148 | 149 | org.webjars.bower 150 | jqcloud2 151 | 2.0.3 152 | 153 | 154 | org.webjars.bower 155 | momentjs 156 | 2.20.1 157 | 158 | 159 | org.webjars.bower 160 | Autolinker.js 161 | 1.6.0 162 | 163 | 164 | 165 | 166 | org.webjars.bower 167 | mustache 168 | 2.3.0 169 | 170 | 171 | 172 | org.projectlombok 173 | lombok 174 | 1.18.24 175 | 176 | 177 | javax.xml.bind 178 | jaxb-api 179 | 2.2.12 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | org.springdoc 208 | springdoc-openapi-starter-webmvc-ui 209 | 2.0.2 210 | 211 | 212 | 213 | org.springframework.boot 214 | spring-boot-loader-tools 215 | 216 | 217 | 218 | org.springframework.boot 219 | spring-boot-starter-test 220 | test 221 | 222 | 223 | 224 | 225 | 226 | 227 | org.springframework.boot 228 | spring-boot-maven-plugin 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | ${project.artifactId} 244 | 245 | 246 | 247 | 248 | 249 | spring-snapshots 250 | Spring Snapshots 251 | https://repo.spring.io/snapshot 252 | 253 | true 254 | 255 | 256 | 257 | spring-milestones 258 | Spring Milestones 259 | https://repo.spring.io/milestone 260 | 261 | false 262 | 263 | 264 | 265 | 266 | 267 | 268 | spring-snapshots 269 | Spring Snapshots 270 | https://repo.spring.io/snapshot 271 | 272 | true 273 | 274 | 275 | 276 | spring-milestones 277 | Spring Milestones 278 | https://repo.spring.io/milestone 279 | 280 | false 281 | 282 | 283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/Booter.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Booter { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Booter.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/aop/CategoryInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.application.aop; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.web.servlet.HandlerInterceptor; 7 | //import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 8 | 9 | import com.millky.blog.infrastructure.dao.CategoryDao; 10 | 11 | public class CategoryInterceptor implements HandlerInterceptor {//extends HandlerInterceptorAdapter { 12 | 13 | private CategoryDao categoryDao; 14 | 15 | public CategoryInterceptor(CategoryDao categoryDao) { 16 | this.categoryDao = categoryDao; 17 | } 18 | 19 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 20 | 21 | request.setAttribute("_CATEGORY_LIST", categoryDao.findAll()); 22 | 23 | return true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/aop/UserSessionArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.application.aop; 2 | 3 | import org.springframework.core.MethodParameter; 4 | import org.springframework.web.bind.support.WebDataBinderFactory; 5 | import org.springframework.web.context.request.NativeWebRequest; 6 | import org.springframework.web.context.request.RequestAttributes; 7 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 8 | import org.springframework.web.method.support.ModelAndViewContainer; 9 | 10 | import com.millky.blog.domain.model.UserSession; 11 | 12 | public class UserSessionArgumentResolver implements HandlerMethodArgumentResolver { 13 | 14 | @Override 15 | public boolean supportsParameter(MethodParameter parameter) { 16 | return UserSession.class.isAssignableFrom(parameter.getParameterType()); 17 | } 18 | 19 | @Override 20 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 21 | NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 22 | 23 | return webRequest.getAttribute("_USER", RequestAttributes.SCOPE_REQUEST); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/aop/UserSessionInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.application.aop; 2 | 3 | import com.millky.blog.domain.model.UserSession; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContext; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.security.oauth2.core.user.DefaultOAuth2User; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | //import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 11 | 12 | import jakarta.servlet.http.HttpServletRequest; 13 | import jakarta.servlet.http.HttpServletResponse; 14 | 15 | @Slf4j 16 | public class UserSessionInterceptor implements HandlerInterceptor { // extends HandlerInterceptorAdapter { 17 | 18 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 19 | 20 | try { 21 | SecurityContext context = SecurityContextHolder.getContext(); 22 | Authentication authentication = context.getAuthentication(); 23 | String currentPrincipalName = authentication.getName(); 24 | if (!currentPrincipalName.equals("anonymousUser")) { 25 | DefaultOAuth2User userDetails = (DefaultOAuth2User) authentication.getPrincipal(); 26 | request.setAttribute("_USER", new UserSession(currentPrincipalName, "", (String) userDetails.getAttributes().get("name"))); 27 | } 28 | } catch (Exception e) { 29 | e.printStackTrace(); 30 | } 31 | 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/configuration/BlogConfig.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.application.configuration; 2 | 3 | import com.millky.blog.domain.service.factory.SearchServiceFactory; 4 | import org.springframework.beans.factory.FactoryBean; 5 | import org.springframework.beans.factory.config.ServiceLocatorFactoryBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class BlogConfig { 11 | 12 | @Bean 13 | public FactoryBean serviceLocatorFactoryBean() { 14 | ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean(); 15 | factoryBean.setServiceLocatorInterface(SearchServiceFactory.class); 16 | return factoryBean; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/configuration/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.application.configuration; 2 | 3 | import io.swagger.v3.oas.models.OpenAPI; 4 | import io.swagger.v3.oas.models.info.Info; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class OpenApiConfig { 10 | @Bean 11 | public OpenAPI openAPI() { 12 | 13 | Info info = new Info() 14 | .version("v1.0.0") 15 | .title("SpringBlog from Millky") 16 | .description("Spring Boot base Open-source Blog - APIs"); 17 | // .contact("origoni@live.com"); 18 | 19 | return new OpenAPI() 20 | .info(info); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/configuration/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.application.configuration; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | //import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.web.WebSecurityConfigurer; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | //import org.springframework.security.config.annotation.web.builders.WebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | //import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; 12 | import org.springframework.security.web.SecurityFilterChain; 13 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 14 | 15 | import java.util.List; 16 | 17 | @Configuration 18 | @EnableWebSecurity 19 | public class SecurityConfig { // } extends WebSecurityConfigurerAdapter { 20 | 21 | List publicApis = List.of("/generate/**", "validated/**"); 22 | 23 | 24 | 25 | // @Override 26 | @Bean 27 | // public 28 | SecurityFilterChain configure(HttpSecurity http) throws Exception { 29 | // @formatter:off 30 | 31 | 32 | return http 33 | 34 | 35 | .oauth2Login() 36 | .loginPage("/user/login") 37 | .and() 38 | .logout() 39 | .logoutUrl("/user/logout") 40 | .logoutSuccessUrl("/") 41 | .and() 42 | .authorizeRequests() 43 | 44 | .requestMatchers(new AntPathRequestMatcher("/**/write*")).authenticated() 45 | .requestMatchers(new AntPathRequestMatcher("/**/edit*")).authenticated() 46 | .requestMatchers(new AntPathRequestMatcher("/**/delete*")).authenticated() 47 | .requestMatchers(new AntPathRequestMatcher("/**")).permitAll() 48 | // .antMatchers("/**/write*", "/**/edit*", "/**/delete*").authenticated() 49 | // .antMatchers("/**").permitAll() 50 | .and() 51 | .build(); 52 | // @formatter:on 53 | } 54 | 55 | // @Override 56 | @Bean 57 | public WebSecurityCustomizer webSecurityCustomizer() {// void configure(WebSecurity web) throws Exception { 58 | return (web) -> web.ignoring() .requestMatchers(new AntPathRequestMatcher("/h2-console/**")); 59 | // .requestMatchers(new AntPathRequestMatcher("/**")) 60 | // .requestMatchers(new AntPathRequestMatcher("/swagger-ui.html")); 61 | //antMatchers("/h2-console/**"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/configuration/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | //package com.millky.blog.application.configuration; 2 | // 3 | //import org.springframework.context.annotation.Bean; 4 | //import org.springframework.context.annotation.Configuration; 5 | // 6 | //import io.swagger.annotations.ApiOperation; 7 | //import springfox.documentation.builders.ApiInfoBuilder; 8 | //import springfox.documentation.builders.RequestHandlerSelectors; 9 | //import springfox.documentation.service.ApiInfo; 10 | //import springfox.documentation.spi.DocumentationType; 11 | //import springfox.documentation.spring.web.plugins.Docket; 12 | //import springfox.documentation.swagger2.annotations.EnableSwagger2; 13 | // 14 | //@Configuration 15 | //@EnableSwagger2 16 | //public class SwaggerConfig { 17 | // 18 | // @Bean 19 | // public Docket restApi() { 20 | // // @formatter:off 21 | // return new Docket(DocumentationType.SWAGGER_2) 22 | // .apiInfo(apiInfo()) 23 | // .select() 24 | // // .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) 25 | // .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) 26 | // .build() 27 | // .useDefaultResponseMessages(false); 28 | // // @formatter:on 29 | // } 30 | // 31 | // private ApiInfo apiInfo() { 32 | // // @formatter:off 33 | // return new ApiInfoBuilder() 34 | // .title("SpringBlog from Millky") 35 | // .description("Spring Boot base Open-source Blog - APIs") 36 | // .contact("origoni@live.com") 37 | // .version("1.0.0") 38 | // .build(); 39 | // // @formatter:on 40 | // } 41 | // 42 | //} 43 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/configuration/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.application.configuration; 2 | 3 | import com.millky.blog.application.aop.CategoryInterceptor; 4 | import com.millky.blog.application.aop.UserSessionArgumentResolver; 5 | import com.millky.blog.application.aop.UserSessionInterceptor; 6 | import com.millky.blog.infrastructure.dao.CategoryDao; 7 | //import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 10 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | 13 | import java.util.List; 14 | 15 | @Configuration 16 | public class WebMvcConfig implements WebMvcConfigurer { 17 | 18 | private final CategoryDao categoryDao; 19 | 20 | public WebMvcConfig(CategoryDao categoryDao) { 21 | this.categoryDao = categoryDao; 22 | } 23 | 24 | @Override 25 | public void addInterceptors(InterceptorRegistry registry) { 26 | registry.addInterceptor(new UserSessionInterceptor()); 27 | registry.addInterceptor(new CategoryInterceptor(categoryDao)); 28 | } 29 | 30 | @Override 31 | public void addArgumentResolvers(List argumentResolvers) { 32 | argumentResolvers.add(new UserSessionArgumentResolver()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/application/utility/StringUtility.java: -------------------------------------------------------------------------------- 1 | //package com.millky.blog.application.utility; 2 | // 3 | //public class StringUtility { 4 | // 5 | //} 6 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/UserSession.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class UserSession { 11 | String providerUserId; 12 | String imageUrl; 13 | String displayName; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/command/PostCommand.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model.command; 2 | 3 | import java.util.Date; 4 | 5 | import jakarta.validation.constraints.Min; 6 | import jakarta.validation.constraints.NotNull; 7 | import jakarta.validation.constraints.Size; 8 | 9 | import org.springframework.beans.BeanUtils; 10 | 11 | import com.millky.blog.domain.model.entity.Post; 12 | import com.millky.blog.domain.model.entity.PostTag; 13 | 14 | import lombok.Data; 15 | import lombok.NoArgsConstructor; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | @Data 19 | @Slf4j 20 | @NoArgsConstructor 21 | public class PostCommand { 22 | 23 | int id; 24 | 25 | String userId; 26 | String name; 27 | 28 | @NotNull 29 | @Size(min = 1, max = 255) 30 | String title; 31 | 32 | @Size(max = 255) 33 | String subtitle; 34 | 35 | @NotNull 36 | @Size(min = 1) 37 | String content; 38 | 39 | Date regDate; 40 | 41 | String _csrf; 42 | 43 | @Min(value = 1) 44 | private int categoryId; 45 | 46 | String tags = ""; 47 | 48 | public PostCommand(Post post) { 49 | BeanUtils.copyProperties(post, this); 50 | 51 | for (PostTag postTag : post.getPostTagList()) { 52 | log.debug("postTag = {}", postTag.getTag()); 53 | tags = tags + postTag.getTag().getName() + " "; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/entity/Category.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model.entity; 2 | 3 | import java.util.Date; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.Id; 9 | 10 | import lombok.Data; 11 | 12 | @Data 13 | @Entity 14 | public class Category { 15 | 16 | @Id 17 | @GeneratedValue 18 | private int id; 19 | 20 | @Column(unique = true) 21 | private String name; 22 | 23 | private Date regDate; 24 | 25 | private int postCount; 26 | 27 | private int publicPostCount; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model.entity; 2 | 3 | import java.util.Date; 4 | 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.Id; 8 | 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Data 13 | @Entity 14 | @NoArgsConstructor 15 | // @JsonRootName(value = "comment") 16 | public class Comment { 17 | @Id 18 | @GeneratedValue 19 | int id; 20 | 21 | // @ManyToOne(fetch = FetchType.LAZY) 22 | // @JoinColumn(name = "postId", insertable = false, updatable = false) 23 | // Post post; 24 | int postId; 25 | 26 | String userId; 27 | String name; 28 | 29 | String content; 30 | 31 | Date regDate; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/entity/Post.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model.entity; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import jakarta.persistence.CascadeType; 7 | import jakarta.persistence.Column; 8 | import jakarta.persistence.Entity; 9 | import jakarta.persistence.FetchType; 10 | import jakarta.persistence.GeneratedValue; 11 | import jakarta.persistence.Id; 12 | import jakarta.persistence.JoinColumn; 13 | import jakarta.persistence.Lob; 14 | import jakarta.persistence.ManyToOne; 15 | import jakarta.persistence.OneToMany; 16 | 17 | import org.springframework.beans.BeanUtils; 18 | 19 | import com.millky.blog.domain.model.command.PostCommand; 20 | 21 | import lombok.Data; 22 | import lombok.NoArgsConstructor; 23 | 24 | @Data 25 | @Entity 26 | @NoArgsConstructor 27 | public class Post { 28 | 29 | @Id 30 | @GeneratedValue 31 | int id; 32 | 33 | String userId; 34 | String name; 35 | 36 | @Column(nullable = false) 37 | String title; 38 | 39 | String subtitle; 40 | 41 | @Lob 42 | String content; 43 | 44 | Date regDate; 45 | 46 | Date updateDate; 47 | 48 | private int categoryId; 49 | 50 | @ManyToOne(fetch = FetchType.LAZY) 51 | @JoinColumn(name = "categoryId", insertable = false, updatable = false) 52 | private Category category; 53 | 54 | @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = { CascadeType.MERGE }) 55 | private List postTagList; 56 | 57 | public Post(PostCommand postCommand) { 58 | BeanUtils.copyProperties(postCommand, this); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/entity/PostTag.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model.entity; 2 | 3 | import java.util.Date; 4 | 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.FetchType; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | 12 | import lombok.Data; 13 | import lombok.NoArgsConstructor; 14 | import lombok.ToString; 15 | 16 | @Data 17 | @ToString(exclude = "post") 18 | @Entity 19 | @NoArgsConstructor 20 | public class PostTag { 21 | 22 | public PostTag(int postId, int tagId) { 23 | this.regDate = new Date(); 24 | this.postId = postId; 25 | this.tagId = tagId; 26 | } 27 | 28 | @Id 29 | @GeneratedValue 30 | private int id; 31 | 32 | @ManyToOne(fetch = FetchType.LAZY) 33 | @JoinColumn(name = "postId", insertable = false, updatable = false) 34 | private Post post; 35 | 36 | private int postId; 37 | 38 | @ManyToOne 39 | @JoinColumn(name = "tagId", insertable = false, updatable = false) 40 | private Tag tag; 41 | 42 | private int tagId; 43 | 44 | private Date regDate; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/entity/Tag.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model.entity; 2 | 3 | import java.util.Date; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.Id; 9 | 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Data 14 | @Entity 15 | @NoArgsConstructor 16 | public class Tag { 17 | 18 | public Tag(String tagName) { 19 | this.name = tagName; 20 | this.updateDate = new Date(); 21 | this.regDate = new Date(); 22 | } 23 | 24 | @Id 25 | @GeneratedValue 26 | private int id; 27 | 28 | @Column(unique = true) 29 | private String name; 30 | 31 | private int useCount; 32 | 33 | private Date updateDate; 34 | 35 | private Date regDate; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/model/exception/IllegalUserException.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.model.exception; 2 | 3 | public class IllegalUserException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 5372850228246790558L; 6 | 7 | public IllegalUserException() { 8 | super(); 9 | } 10 | 11 | public IllegalUserException(String message) { 12 | super(message); 13 | } 14 | 15 | public IllegalUserException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public IllegalUserException(Throwable cause) { 20 | super(cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/repository/CategoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.repository; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.stream.Collectors; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import com.millky.blog.domain.model.entity.Category; 11 | import com.millky.blog.infrastructure.dao.CategoryDao; 12 | 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | @Slf4j 16 | @Repository 17 | public class CategoryRepository { 18 | 19 | @Autowired 20 | private CategoryDao categoryDao; 21 | 22 | public Category getCategory(int categoryId) { 23 | 24 | Category category = categoryDao.findById(categoryId).orElse(null); 25 | log.debug("category = {}", category); 26 | 27 | return category; 28 | } 29 | 30 | public List getCategoryList() { 31 | 32 | List categoryList = categoryDao.findAll(); 33 | log.debug("categoryList = {}", categoryList); 34 | 35 | return categoryList; 36 | } 37 | 38 | public Map getCategoryMap() { 39 | return getCategoryList().stream().collect(Collectors.toMap(Category::getId, Category::getName)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/repository/PostRepository.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.repository; 2 | 3 | import java.util.Date; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import com.millky.blog.domain.model.UserSession; 11 | import com.millky.blog.domain.model.command.PostCommand; 12 | import com.millky.blog.domain.model.entity.Post; 13 | import com.millky.blog.domain.model.exception.IllegalUserException; 14 | import com.millky.blog.infrastructure.dao.PostDao; 15 | 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import jakarta.persistence.EntityNotFoundException; 19 | 20 | @Slf4j 21 | @Repository 22 | public class PostRepository { 23 | 24 | @Autowired 25 | private PostDao postDao; 26 | 27 | public Page getPostList(Pageable pageable) { 28 | Page postPage = postDao.findAll(pageable); 29 | log.debug("postPage = {}", postPage); 30 | 31 | return postPage; 32 | } 33 | 34 | public Page getPostList(Pageable pageable, int categoryId) { 35 | return postDao.findByCategoryId(categoryId, pageable); 36 | } 37 | 38 | public Page getPostList(Pageable pageable, String tagName) { 39 | return postDao.findByPostTagListTagName(tagName, pageable); 40 | } 41 | 42 | public Post getPostById(int id) throws IllegalArgumentException { 43 | Post post = postDao.findById(id) 44 | .orElseThrow(() -> new EntityNotFoundException("ERROR Post Not Found ID=" + id)); 45 | 46 | // .orElse(null); 47 | // 48 | // if (post == null) { 49 | // throw new IllegalArgumentException("Post Not Found."); 50 | // } 51 | 52 | return post; 53 | } 54 | 55 | public Post writePost(Post post) { 56 | post.setRegDate(new Date()); 57 | post.setUpdateDate(new Date()); 58 | 59 | return postDao.save(post); 60 | } 61 | 62 | public void deletePost(int id) throws IllegalUserException, IllegalArgumentException { 63 | 64 | postDao.deleteById(id); 65 | } 66 | 67 | public Post editPost(PostCommand postCommand) { 68 | Post post = getPostById(postCommand.getId()); 69 | 70 | post.setUpdateDate(new Date()); 71 | post.setTitle(postCommand.getTitle()); 72 | post.setSubtitle(postCommand.getSubtitle()); 73 | post.setContent(postCommand.getContent()); 74 | post.setCategoryId(postCommand.getCategoryId()); 75 | 76 | return post; 77 | } 78 | 79 | public Post findByIdAndUser(int id, UserSession user) throws RuntimeException { 80 | if (isThisUserPostWriter(user, id)) 81 | return getPostById(id); 82 | else 83 | throw new IllegalUserException("Not the Writer."); 84 | } 85 | 86 | public boolean isThisUserPostWriter(UserSession user, int id) throws IllegalArgumentException { 87 | Post post = getPostById(id); 88 | 89 | return post.getUserId().equals(user.getProviderUserId()); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/repository/PostSearchRepository.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.repository; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.stereotype.Repository; 7 | import org.springframework.web.util.HtmlUtils; 8 | 9 | import com.millky.blog.domain.model.entity.Post; 10 | import com.millky.blog.infrastructure.dao.PostDao; 11 | 12 | @Repository 13 | public class PostSearchRepository { 14 | 15 | @Autowired 16 | private PostDao postDao; 17 | 18 | public Page findPostByTitle(String query, Pageable pageable) { 19 | 20 | return postDao.findByTitleContainingOrSubtitleContaining(query, query, pageable); 21 | } 22 | 23 | public Page findPostByContent(String query, Pageable pageable) { 24 | 25 | return postDao.findByContentContaining(query, pageable); 26 | } 27 | 28 | public Page findPostByTitleAndContent(String query, Pageable pageable) { 29 | 30 | return postDao.findByTitleContainingOrSubtitleContainingOrContentContaining(query, query, 31 | HtmlUtils.htmlEscape(query), pageable); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/repository/PostTagRepository.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.repository; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import com.millky.blog.domain.model.entity.PostTag; 10 | import com.millky.blog.infrastructure.dao.PostTagDao; 11 | 12 | @Repository 13 | public class PostTagRepository { 14 | 15 | @Autowired 16 | PostTagDao postTagDao; 17 | 18 | @Autowired 19 | TagRepository tagRepository; 20 | 21 | public void insertPostTag(PostTag postTag) { 22 | postTag.setRegDate(new Date()); 23 | postTagDao.save(postTag); 24 | 25 | tagRepository.increaseUseCount(postTag.getTagId()); 26 | } 27 | 28 | public List findByPostId(int postId) { 29 | return postTagDao.findByPostId(postId); 30 | } 31 | 32 | public void deletePostTag(PostTag postTag) { 33 | postTagDao.deleteById(postTag.getId()); 34 | 35 | tagRepository.decreaseUseCount(postTag.getTagId()); 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/repository/TagRepository.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.repository; 2 | 3 | import java.util.Date; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import com.millky.blog.domain.model.entity.Tag; 11 | import com.millky.blog.infrastructure.dao.TagDao; 12 | 13 | import jakarta.persistence.EntityNotFoundException; 14 | 15 | @Repository 16 | public class TagRepository { 17 | 18 | @Autowired 19 | TagDao tagDao; 20 | 21 | public Tag findTagByTagName(String tagName) { 22 | 23 | return tagDao.findByName(tagName); 24 | } 25 | 26 | public Tag createTag(Tag tag) { 27 | 28 | tag.setRegDate(new Date()); 29 | return tagDao.save(tag); 30 | } 31 | 32 | public void increaseUseCount(int tagIdx) { 33 | 34 | Tag tag = tagDao.findById(tagIdx) 35 | .orElseThrow(() -> new EntityNotFoundException("ERROR Tag Not Found tagIdx=" + tagIdx)); 36 | 37 | tag.setUpdateDate(new Date()); 38 | tag.setUseCount(tag.getUseCount() + 1); 39 | } 40 | 41 | public void decreaseUseCount(int tagIdx) { 42 | Tag tag = tagDao.findById(tagIdx) 43 | .orElseThrow(() -> new EntityNotFoundException("ERROR Tag Not Found tagIdx=" + tagIdx)); 44 | 45 | tag.setUpdateDate(new Date()); 46 | tag.setUseCount(tag.getUseCount() - 1); 47 | } 48 | 49 | public Page findAll(Pageable pageable) { 50 | 51 | Page tags = tagDao.findAll(pageable); 52 | return tags; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/service/PostSearchService.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.service; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | 6 | import com.millky.blog.domain.model.entity.Post; 7 | 8 | public interface PostSearchService { 9 | 10 | Page findPost(String query, Pageable pageable); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/service/PostService.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.service; 2 | 3 | import jakarta.transaction.Transactional; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import com.millky.blog.domain.model.UserSession; 9 | import com.millky.blog.domain.model.command.PostCommand; 10 | import com.millky.blog.domain.model.entity.Post; 11 | import com.millky.blog.domain.model.exception.IllegalUserException; 12 | import com.millky.blog.domain.repository.PostRepository; 13 | 14 | @Service 15 | public class PostService { 16 | 17 | @Autowired 18 | private PostRepository postRepository; 19 | 20 | @Autowired 21 | private TagService tagService; 22 | 23 | @Transactional 24 | public Post writePost(PostCommand postCommand, UserSession user) { 25 | 26 | postCommand.setUserId(user.getProviderUserId()); 27 | postCommand.setName(user.getDisplayName()); 28 | 29 | Post post = postRepository.writePost(new Post(postCommand)); 30 | 31 | postCommand.setId(post.getId()); 32 | 33 | tagService.insertPostTag(postCommand); 34 | 35 | return post; 36 | } 37 | 38 | @Transactional 39 | public Post editPost(PostCommand postCommand, UserSession user) throws RuntimeException { 40 | 41 | if (!postRepository.isThisUserPostWriter(user, postCommand.getId())) { 42 | throw new IllegalUserException("Not the Writer."); 43 | } 44 | 45 | Post post = postRepository.editPost(postCommand); 46 | 47 | tagService.updatePostTag(postCommand); 48 | 49 | return post; 50 | } 51 | 52 | @Transactional 53 | public void deletePost(int postId, UserSession user) throws IllegalUserException, IllegalArgumentException { 54 | 55 | if (!postRepository.isThisUserPostWriter(user, postId)) { 56 | throw new IllegalUserException("Not the Writer."); 57 | } 58 | 59 | tagService.deletePostTagByPostId(postId); 60 | 61 | postRepository.deletePost(postId); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/service/TagService.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.service; 2 | 3 | import java.util.HashSet; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.StringTokenizer; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import com.millky.blog.domain.model.command.PostCommand; 12 | import com.millky.blog.domain.model.entity.PostTag; 13 | import com.millky.blog.domain.model.entity.Tag; 14 | import com.millky.blog.domain.repository.PostTagRepository; 15 | import com.millky.blog.domain.repository.TagRepository; 16 | 17 | import lombok.extern.slf4j.Slf4j; 18 | 19 | @Slf4j 20 | @Service 21 | public class TagService { 22 | 23 | @Autowired 24 | private TagRepository tagRepository; 25 | 26 | @Autowired 27 | private PostTagRepository postTagRepository; 28 | 29 | public void insertPostTag(PostCommand postCommand) { 30 | 31 | HashSet hashSet = tagNamesToHashSet(postCommand.getTags()); 32 | 33 | log.debug("hashSet = {}", hashSet); 34 | 35 | insertTag(postCommand.getId(), hashSet); 36 | } 37 | 38 | public void updatePostTag(PostCommand postCommand) { 39 | 40 | List oldPostTagList = postTagRepository.findByPostId(postCommand.getId()); 41 | HashSet newTagHashSet = tagNamesToHashSet(postCommand.getTags()); 42 | 43 | Iterator newTagIterator = newTagHashSet.iterator(); 44 | while (newTagIterator.hasNext()) { 45 | String newTag = newTagIterator.next(); 46 | Iterator oldTagIterator = oldPostTagList.iterator(); 47 | while (oldTagIterator.hasNext()) { 48 | String oldTag = oldTagIterator.next().getTag().getName(); 49 | if (newTag.equals(oldTag)) { 50 | newTagIterator.remove(); 51 | oldTagIterator.remove(); 52 | } 53 | } 54 | } 55 | 56 | if (newTagHashSet != null) { 57 | insertTag(postCommand.getId(), newTagHashSet); 58 | } 59 | if (oldPostTagList != null) { 60 | deleteTag(oldPostTagList); 61 | } 62 | 63 | } 64 | 65 | public void deletePostTagByPostId(int postId) { 66 | List postTagList = postTagRepository.findByPostId(postId); 67 | 68 | deleteTag(postTagList); 69 | } 70 | 71 | private HashSet tagNamesToHashSet(String postTagNames) { 72 | 73 | StringTokenizer tokenTag = new StringTokenizer(postTagNames.trim().replaceAll("[ ]+", " "), " "); 74 | 75 | HashSet hashSet = new HashSet<>(); 76 | 77 | while (tokenTag.hasMoreElements()) { 78 | hashSet.add(tokenTag.nextToken()); 79 | if (hashSet.size() >= 10) { 80 | break; 81 | } 82 | } 83 | 84 | return hashSet; 85 | } 86 | 87 | private void insertTag(int postId, HashSet hashSet) { 88 | for (String tagName : hashSet) { 89 | if (tagName.equals("")) { 90 | continue; 91 | } 92 | 93 | Tag tag = tagRepository.findTagByTagName(tagName); 94 | if (tag == null) { 95 | tag = tagRepository.createTag(new Tag(tagName)); 96 | } 97 | 98 | postTagRepository.insertPostTag(new PostTag(postId, tag.getId())); 99 | } 100 | } 101 | 102 | private void deleteTag(List postTagList) { 103 | for (PostTag postTag : postTagList) { 104 | this.postTagRepository.deletePostTag(postTag); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/service/factory/SearchServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.service.factory; 2 | 3 | import com.millky.blog.domain.service.PostSearchService; 4 | 5 | public interface SearchServiceFactory { 6 | 7 | PostSearchService getSearchService(String selector); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/service/impl/PostContentSearchService.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.service.impl; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.web.util.HtmlUtils; 8 | 9 | import com.millky.blog.domain.model.entity.Post; 10 | import com.millky.blog.domain.repository.PostSearchRepository; 11 | import com.millky.blog.domain.service.PostSearchService; 12 | 13 | @Service("contents") 14 | public class PostContentSearchService implements PostSearchService { 15 | 16 | @Autowired 17 | private PostSearchRepository postSearchRepository; 18 | 19 | @Override 20 | public Page findPost(String query, Pageable pageable) { 21 | 22 | return postSearchRepository.findPostByContent(HtmlUtils.htmlEscape(query), pageable); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/service/impl/PostTitleAndContentSearchService.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.service.impl; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.stereotype.Service; 7 | 8 | import com.millky.blog.domain.model.entity.Post; 9 | import com.millky.blog.domain.repository.PostSearchRepository; 10 | import com.millky.blog.domain.service.PostSearchService; 11 | 12 | @Service("titleAndContents") 13 | public class PostTitleAndContentSearchService implements PostSearchService { 14 | 15 | @Autowired 16 | private PostSearchRepository postSearchRepository; 17 | 18 | @Override 19 | public Page findPost(String query, Pageable pageable) { 20 | 21 | return postSearchRepository.findPostByTitleAndContent(query, pageable); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/domain/service/impl/PostTitleSearchService.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.domain.service.impl; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.stereotype.Service; 7 | 8 | import com.millky.blog.domain.model.entity.Post; 9 | import com.millky.blog.domain.repository.PostSearchRepository; 10 | import com.millky.blog.domain.service.PostSearchService; 11 | 12 | @Service("title") 13 | public class PostTitleSearchService implements PostSearchService { 14 | 15 | @Autowired 16 | private PostSearchRepository postSearchRepository; 17 | 18 | @Override 19 | public Page findPost(String query, Pageable pageable) { 20 | 21 | return postSearchRepository.findPostByTitle(query, pageable); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/infrastructure/dao/CategoryDao.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.infrastructure.dao; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import com.millky.blog.domain.model.entity.Category; 6 | 7 | public interface CategoryDao extends JpaRepository{ 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/infrastructure/dao/CommentDao.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.infrastructure.dao; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import com.millky.blog.domain.model.entity.Comment; 8 | 9 | public interface CommentDao extends JpaRepository { 10 | 11 | List findByPostId(int postId); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/infrastructure/dao/PostDao.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.infrastructure.dao; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import com.millky.blog.domain.model.entity.Post; 8 | 9 | public interface PostDao extends JpaRepository { 10 | 11 | public Page findByCategoryId(int categoryId, Pageable pageable); 12 | 13 | public Page findByPostTagListTagName(String tagName, Pageable pageable); 14 | 15 | public Page findByContentContaining(String query, Pageable pageable); 16 | 17 | public Page findByTitleContainingOrSubtitleContaining(String title, String subtitle, Pageable pageable); 18 | 19 | public Page findByTitleContainingOrSubtitleContainingOrContentContaining(String title, String subtitle, 20 | String content, Pageable pageable); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/infrastructure/dao/PostTagDao.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.infrastructure.dao; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import com.millky.blog.domain.model.entity.PostTag; 8 | 9 | public interface PostTagDao extends JpaRepository { 10 | 11 | List findByTagName(String tagName); 12 | 13 | List findByPostId(int postId); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/infrastructure/dao/TagDao.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.infrastructure.dao; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import com.millky.blog.domain.model.entity.Tag; 6 | 7 | public interface TagDao extends JpaRepository { 8 | 9 | Tag findByName(String tagName); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/rest/CommentRestController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.rest; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.ResponseStatus; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import com.millky.blog.domain.model.UserSession; 16 | import com.millky.blog.domain.model.entity.Comment; 17 | import com.millky.blog.domain.model.exception.IllegalUserException; 18 | import com.millky.blog.infrastructure.dao.CommentDao; 19 | 20 | import jakarta.persistence.EntityNotFoundException; 21 | 22 | @RestController 23 | public class CommentRestController { 24 | 25 | @Autowired 26 | CommentDao commentDao; 27 | 28 | @RequestMapping(value = "/comments", method = RequestMethod.GET) 29 | public List list(@RequestParam(value = "postId", required = true) int postId) { 30 | return commentDao.findByPostId(postId); 31 | } 32 | 33 | @RequestMapping(value = "/comments", method = RequestMethod.POST) 34 | @ResponseStatus(HttpStatus.CREATED) 35 | public Comment save(@RequestParam(value = "postId", required = true) int postId, 36 | @RequestParam(value = "content", required = true) String content, UserSession user) { 37 | 38 | // 빠르게 테스트하기위해 이렇게 만들었지만, 글에 권한이 있거나 하면 훨씬 복잡해짐. 39 | Comment comment = new Comment(); 40 | comment.setPostId(postId); 41 | comment.setContent(content); 42 | comment.setUserId(user.getProviderUserId()); 43 | comment.setName(user.getDisplayName()); 44 | comment.setRegDate(new Date()); 45 | 46 | return commentDao.save(comment); 47 | } 48 | 49 | @RequestMapping(value = "/comments/{id}", method = RequestMethod.DELETE) 50 | @ResponseStatus(HttpStatus.NO_CONTENT) 51 | public void delete(@RequestParam(value = "postId", required = true) int postId, @PathVariable int id, 52 | UserSession user) { 53 | 54 | // 확인해야 할 것이 많지만.. 최소 같은작성자인지는 확인하자. 55 | Comment comment = commentDao.findById(id) 56 | .orElseThrow(() -> new EntityNotFoundException("ERROR Comment Not Found ID=" + id)); 57 | if (comment.getUserId().equals(user.getProviderUserId())) { 58 | commentDao.deleteById(id); 59 | } else { 60 | throw new IllegalUserException("Not the Writer."); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/rest/TagRecord.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.rest; 2 | 3 | public record TagRecord() { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/rest/TagRestController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.rest; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import jakarta.validation.Valid; 7 | import jakarta.validation.constraints.Max; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.data.domain.PageRequest; 11 | import org.springframework.data.domain.Pageable; 12 | import org.springframework.data.domain.Sort; 13 | import org.springframework.data.domain.Sort.Direction; 14 | import org.springframework.data.web.PageableDefault; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.web.bind.annotation.ModelAttribute; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RequestMethod; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | import com.millky.blog.domain.model.entity.Tag; 22 | import com.millky.blog.domain.repository.TagRepository; 23 | 24 | //import io.swagger.annotations.Api; 25 | //import io.swagger.annotations.ApiModel; 26 | //import io.swagger.annotations.ApiModelProperty; 27 | //import io.swagger.annotations.ApiOperation; 28 | //import io.swagger.annotations.ApiParam; 29 | import lombok.Data; 30 | 31 | @RestController 32 | @RequestMapping(value = { "/api/v1" }, produces = MediaType.APPLICATION_JSON_VALUE) 33 | //@Api(value = "Tag API") 34 | public class TagRestController { 35 | 36 | @Autowired 37 | private TagRepository tagRepository; 38 | 39 | @RequestMapping(value = "/tags", method = RequestMethod.GET) 40 | public List tags(@PageableDefault(sort = { "updateDate" }, direction = Direction.DESC) Pageable pageable) { 41 | 42 | return tagRepository.findAll(pageable).getContent(); 43 | } 44 | 45 | // @ApiOperation(value = "태그 리스트 for Cloud", notes = "태그 클라우드에서 사용할 태그 리스트를 가지고 옵니다.") 46 | @RequestMapping(value = "/tag-cloud", method = RequestMethod.GET) 47 | public List tagCloud(@ModelAttribute @Valid TagCloudCommand command) { 48 | 49 | Sort sort = Sort.by(Sort.Order.desc( "updateDate")); 50 | // new Sort(new Sort.Order(Sort.Direction.DESC, "updateDate")); 51 | Pageable pageable = PageRequest.of(0, command.getSize(), sort); 52 | // new PageRequest(0, command.getSize(), sort); 53 | return tagRepository.findAll(pageable).getContent().stream().map(TagCloud::new).collect(Collectors.toList()); 54 | } 55 | 56 | @Data 57 | static class TagCloudCommand { 58 | @Max(value = 100) 59 | // @ApiParam(required = false, value = "한번에 가지고 올 태그의 수
(기본값 : 20, 최대 : 100)") 60 | int size = 20; 61 | } 62 | 63 | @Data 64 | // @ApiModel(value = "TagCloud") 65 | static class TagCloud { 66 | TagCloud(Tag tag) { 67 | text = tag.getName(); 68 | weight = tag.getUseCount(); 69 | link = "/tag/" + tag.getName() + "/post/list"; 70 | } 71 | 72 | // @ApiModelProperty(value = "태그명") 73 | String text; 74 | 75 | // @ApiModelProperty(value = "태그의 사용 개수") 76 | int weight; 77 | 78 | // @ApiModelProperty(value = "태그를 눌렀을때 이동할 링") 79 | String link; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/CategoryController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web; 2 | 3 | import java.util.Date; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | 12 | import com.millky.blog.domain.model.entity.Category; 13 | import com.millky.blog.infrastructure.dao.CategoryDao; 14 | 15 | @Controller 16 | public class CategoryController { 17 | 18 | @Autowired 19 | private CategoryDao categoryDao; 20 | 21 | @ResponseBody 22 | @RequestMapping(value = "/category/add", method = RequestMethod.POST) 23 | public Category add(@RequestParam(value = "categoryName", required = true) String categoryName) { 24 | 25 | Category category = new Category(); 26 | category.setName(categoryName); 27 | category.setRegDate(new Date()); 28 | 29 | return categoryDao.save(category); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/HelloController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | 7 | @Controller 8 | public class HelloController { 9 | 10 | @RequestMapping({ "/", "/hello" }) 11 | public String index(Model model) { 12 | model.addAttribute("name", "SpringBlog from Millky"); 13 | return "hello"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/UserController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.core.ResolvableType; 5 | import org.springframework.security.oauth2.client.registration.ClientRegistration; 6 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | @Controller 16 | public class UserController { 17 | 18 | private static String authorizationRequestBaseUri = "/oauth2/authorization"; 19 | Map oauth2AuthenticationUrls = new HashMap<>(); 20 | 21 | @Autowired 22 | private ClientRegistrationRepository clientRegistrationRepository; 23 | 24 | @RequestMapping(value = "/user/login", method = RequestMethod.GET) 25 | public String getLoginPage(Model model) { 26 | Iterable clientRegistrations = null; 27 | ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository) 28 | .as(Iterable.class); 29 | if (type != ResolvableType.NONE && 30 | ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { 31 | clientRegistrations = (Iterable) clientRegistrationRepository; 32 | } 33 | 34 | clientRegistrations.forEach(registration -> 35 | oauth2AuthenticationUrls.put(registration.getClientName(), 36 | authorizationRequestBaseUri + "/" + registration.getRegistrationId())); 37 | model.addAttribute("urls", oauth2AuthenticationUrls); 38 | 39 | return "connect/login"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/post/PostDeleteController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web.post; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | 8 | import com.millky.blog.domain.model.UserSession; 9 | import com.millky.blog.domain.service.PostService; 10 | 11 | @Controller 12 | @RequestMapping("/post") 13 | public class PostDeleteController { 14 | 15 | @Autowired 16 | private PostService postService; 17 | 18 | @RequestMapping("/{id}/delete") 19 | public String delete(@PathVariable int id, UserSession user) { 20 | 21 | postService.deletePost(id, user); 22 | 23 | return "redirect:/post/list"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/post/PostEditController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web.post; 2 | 3 | import jakarta.validation.Valid; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.validation.BindingResult; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestMethod; 12 | 13 | import com.millky.blog.domain.model.UserSession; 14 | import com.millky.blog.domain.model.command.PostCommand; 15 | import com.millky.blog.domain.repository.CategoryRepository; 16 | import com.millky.blog.domain.repository.PostRepository; 17 | import com.millky.blog.domain.service.PostService; 18 | 19 | @Controller 20 | @RequestMapping("/post") 21 | public class PostEditController { 22 | 23 | @Autowired 24 | private PostRepository postRepository; 25 | 26 | @Autowired 27 | private PostService postService; 28 | 29 | @Autowired 30 | private CategoryRepository categoryRepository; 31 | 32 | @RequestMapping(value = "/{id}/edit", method = RequestMethod.GET) 33 | public String editor(Model model, @PathVariable int id, UserSession user) { 34 | 35 | model.addAttribute("postCommand", new PostCommand(postRepository.findByIdAndUser(id, user))); 36 | model.addAttribute("categoryMap", categoryRepository.getCategoryMap()); 37 | 38 | return "post/form"; 39 | } 40 | 41 | @RequestMapping(value = "/{id}/edit", method = RequestMethod.POST) 42 | public String edit(@Valid PostCommand post, BindingResult bindingResult, UserSession user, Model model) { 43 | 44 | model.addAttribute("categoryMap", categoryRepository.getCategoryMap()); 45 | 46 | if (bindingResult.hasErrors()) { 47 | return "post/form"; 48 | } 49 | 50 | return "redirect:/post/" + postService.editPost(post, user).getId(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/post/PostSearchController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web.post; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Sort.Direction; 7 | import org.springframework.data.web.PageableDefault; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | 13 | import com.millky.blog.domain.model.entity.Post; 14 | import com.millky.blog.domain.service.PostSearchService; 15 | import com.millky.blog.domain.service.factory.SearchServiceFactory; 16 | 17 | import lombok.extern.slf4j.Slf4j; 18 | 19 | @Slf4j 20 | @Controller 21 | public class PostSearchController { 22 | 23 | @Autowired 24 | private SearchServiceFactory searchServiceFactory; 25 | 26 | private PostSearchService searchService; 27 | 28 | @RequestMapping(value = "/post/search") 29 | public String view(Model model, @RequestParam(defaultValue = "title") String type, 30 | @RequestParam(required = true) String query, 31 | @PageableDefault(sort = { "id" }, direction = Direction.DESC, size = 5) Pageable pageable) { 32 | 33 | log.info("type = {} & query = {}", type, query); 34 | 35 | searchService = searchServiceFactory.getSearchService(type); 36 | 37 | Page postPage = searchService.findPost(query, pageable); 38 | 39 | model.addAttribute("type", type); 40 | model.addAttribute("query", query); 41 | model.addAttribute("postPage", postPage); 42 | 43 | return "post/list"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/post/PostViewController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web.post; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Sort.Direction; 7 | import org.springframework.data.web.PageableDefault; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import com.millky.blog.domain.model.entity.Post; 14 | import com.millky.blog.domain.repository.CategoryRepository; 15 | import com.millky.blog.domain.repository.PostRepository; 16 | 17 | import lombok.extern.slf4j.Slf4j; 18 | 19 | @Slf4j 20 | @Controller 21 | public class PostViewController { 22 | 23 | @Autowired 24 | private PostRepository postRepository; 25 | 26 | @Autowired 27 | private CategoryRepository categoryRepository; 28 | 29 | @RequestMapping("/post/{id}") 30 | public String view(Model model, @PathVariable int id) { 31 | 32 | model.addAttribute("post", postRepository.getPostById(id)); 33 | 34 | log.debug("post = {}", postRepository.getPostById(id)); 35 | 36 | return "post/post"; 37 | } 38 | 39 | @RequestMapping(value = { "/post/list", "/category/{categoryId}/post/list", "/tag/{tagName}/post/list" }) 40 | public String list(Model model, @PathVariable java.util.Optional categoryId, 41 | @PathVariable java.util.Optional tagName, 42 | @PageableDefault(sort = { "id" }, direction = Direction.DESC, size = 5) Pageable pageable) { 43 | 44 | Page postPage; 45 | 46 | if (categoryId.isPresent()) { 47 | postPage = postRepository.getPostList(pageable, categoryId.get()); 48 | model.addAttribute("category", categoryRepository.getCategory(categoryId.get()).getName()); 49 | } else if (tagName.isPresent()) { 50 | postPage = postRepository.getPostList(pageable, tagName.get()); 51 | model.addAttribute("tag", tagName.get()); 52 | } else { 53 | postPage = postRepository.getPostList(pageable); 54 | } 55 | 56 | model.addAttribute("postPage", postPage); 57 | 58 | return "post/list"; 59 | } 60 | 61 | @RequestMapping("/rss/post/list") 62 | public String list(Model model, 63 | @PageableDefault(sort = { "id" }, direction = Direction.DESC, size = 10) Pageable pageable) { 64 | 65 | model.addAttribute("postList", postRepository.getPostList(pageable).getContent()); 66 | 67 | return "post/rss"; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/controller/web/post/PostWriteController.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.controller.web.post; 2 | 3 | import jakarta.validation.Valid; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.validation.BindingResult; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | 12 | import com.millky.blog.domain.model.UserSession; 13 | import com.millky.blog.domain.model.command.PostCommand; 14 | import com.millky.blog.domain.repository.CategoryRepository; 15 | import com.millky.blog.domain.service.PostService; 16 | 17 | @Controller 18 | @RequestMapping("/post/write") 19 | public class PostWriteController { 20 | 21 | @Autowired 22 | private PostService postService; 23 | 24 | @Autowired 25 | private CategoryRepository categoryRepository; 26 | 27 | @RequestMapping(method = RequestMethod.GET) 28 | public String form(PostCommand post, Model model) { 29 | 30 | model.addAttribute("categoryMap", categoryRepository.getCategoryMap()); 31 | 32 | return "post/form"; 33 | } 34 | 35 | @RequestMapping(method = RequestMethod.POST) 36 | public String write(@Valid PostCommand post, BindingResult bindingResult, UserSession user, Model model) { 37 | 38 | model.addAttribute("categoryMap", categoryRepository.getCategoryMap()); 39 | 40 | if (bindingResult.hasErrors()) { 41 | return "post/form"; 42 | } 43 | 44 | return "redirect:/post/" + postService.writePost(post, user).getId(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/support/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.support; 2 | 3 | import java.io.IOException; 4 | 5 | import jakarta.servlet.http.HttpServletResponse; 6 | 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.ControllerAdvice; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | 13 | import com.millky.blog.domain.model.exception.IllegalUserException; 14 | 15 | @Slf4j 16 | @ControllerAdvice 17 | public class GlobalExceptionHandler { 18 | 19 | @ExceptionHandler({ IllegalArgumentException.class }) 20 | void handleIllegalArgumentException(HttpServletResponse response) throws IOException { 21 | log.debug("IllegalArgumentException"); 22 | response.sendError(HttpStatus.BAD_REQUEST.value()); 23 | } 24 | 25 | @ExceptionHandler({ IllegalUserException.class }) 26 | void handleIllegalUserException(HttpServletResponse response) throws IOException { 27 | log.debug("IllegalUserException"); 28 | response.sendError(HttpStatus.FORBIDDEN.value()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/millky/blog/presentation/support/JsonBuilder.java: -------------------------------------------------------------------------------- 1 | package com.millky.blog.presentation.support; 2 | 3 | public class JsonBuilder { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | mvc: 3 | view: 4 | prefix: /WEB-INF/jsp/ 5 | suffix: .jsp 6 | jpa: 7 | generate-ddl: true 8 | show-sql: true 9 | hibernate: 10 | ddl-auto: update 11 | h2: 12 | console: 13 | enabled: true 14 | datasource: 15 | url: jdbc:h2:file:./spring-blog 16 | 17 | security: 18 | oauth2: 19 | client: 20 | registration: 21 | google: 22 | client-id: 564319405493-ldgbknrojsjh4m3lm187df00n5rt24bs.apps.googleusercontent.com 23 | client-secret: 6PnvyM2DObCTtL1CKvj_VTwq 24 | 25 | # "regDate": "2022-12-24T11:58:58.779+09:00" 26 | jackson: 27 | # date-format: yyyy-MM-dd'T'HH:mm:ss.SSSXXX 28 | time-zone : Asia/Seoul 29 | # deserialization: 30 | # unwrap-root-value: true 31 | # serialization: 32 | # wrap-root-value: true 33 | 34 | # https://developers.facebook.com/apps -> 새 앱 추가 -> Facebook 로그인 35 | #spring.security.oauth2.client.registration.facebook.client-id: 487654301400466 36 | #spring.security.oauth2.client.registration.facebook.client-secret: 3ec76bf19fbfeaf187a6673d1cf16411 37 | 38 | # https://console.developers.google.com/ -> 사용자 인증 정보 -> 사용자 인증 정보 만들기 -> OAuth 클라이언트 ID 39 | #spring.security.oauth2.client.registration.google.client-id: 564319405493-ldgbknrojsjh4m3lm187df00n5rt24bs.apps.googleusercontent.com 40 | #spring.security.oauth2.client.registration.google.client-secret: 6PnvyM2DObCTtL1CKvj_VTwq 41 | 42 | #server.tomcat.additional-tld-skip-patterns: jakarta.inject-api.jar,asm.jar,class-model.jar,hk2-runlevel.jar,hk2-utils.jar,jakarta.annotation-api.jar,jakarta.inject.jar,hk2-api.jar,hk2-core.jar,hk2-locator.jar,javassist.jar 43 | 44 | 45 | logging: 46 | level: 47 | com.millky.blog: DEBUG 48 | 49 | 50 | #server: 51 | # error: 52 | # whitelabel: 53 | # enabled: false 54 | 55 | # 56 | #--- 57 | # 58 | #spring: 59 | # profiles: prod 60 | #logging: 61 | # level: 62 | # com.millky.blog: INFO -------------------------------------------------------------------------------- /src/main/resources/build/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.2.3.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'eclipse-wtp' 15 | apply plugin: 'idea' 16 | apply plugin: 'spring-boot' 17 | apply plugin: 'war' 18 | 19 | 20 | war { 21 | baseName = 'spring-blog' 22 | version = '0.0.1-SNAPSHOT' 23 | } 24 | sourceCompatibility = 1.8 25 | targetCompatibility = 1.8 26 | 27 | repositories { 28 | mavenCentral() 29 | } 30 | 31 | configurations { 32 | providedRuntime 33 | } 34 | 35 | dependencies { 36 | compile("org.springframework.boot:spring-boot-starter-aop") 37 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 38 | compile("org.springframework.boot:spring-boot-starter-jdbc") 39 | compile("org.springframework.boot:spring-boot-starter-mobile") 40 | //compile("org.springframework.boot:spring-boot-starter-security") 41 | compile("org.springframework.boot:spring-boot-starter-social-facebook") 42 | compile("org.springframework.boot:spring-boot-starter-social-twitter") 43 | compile("org.springframework.boot:spring-boot-starter-web") 44 | compile("org.springframework.boot:spring-boot-starter-websocket") 45 | runtime("com.h2database:h2") 46 | runtime("mysql:mysql-connector-java") 47 | providedRuntime("org.springframework.boot:spring-boot-starter-tomcat") 48 | testCompile("org.springframework.boot:spring-boot-starter-test") 49 | 50 | compile("javax.servlet:jstl") 51 | compile("org.apache.tomcat.embed:tomcat-embed-jasper") 52 | } 53 | 54 | //eclipse { 55 | // classpath { 56 | // containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') 57 | // containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' 58 | // } 59 | //} 60 | 61 | -------------------------------------------------------------------------------- /src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | required=required 2 | 3 | Min.postCommand.categoryId=required -------------------------------------------------------------------------------- /src/main/resources/messages_ko.properties: -------------------------------------------------------------------------------- 1 | required=\uD544\uC218 2 | 3 | Min.postCommand.categoryId=\uD544\uC218 -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origoni/spring-blog/ad7748d4fa85b3cd30cea2abe2fbc82051829b36/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/image/about-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origoni/spring-blog/ad7748d4fa85b3cd30cea2abe2fbc82051829b36/src/main/resources/static/image/about-bg.jpg -------------------------------------------------------------------------------- /src/main/resources/static/image/contact-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origoni/spring-blog/ad7748d4fa85b3cd30cea2abe2fbc82051829b36/src/main/resources/static/image/contact-bg.jpg -------------------------------------------------------------------------------- /src/main/resources/static/image/home-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origoni/spring-blog/ad7748d4fa85b3cd30cea2abe2fbc82051829b36/src/main/resources/static/image/home-bg.jpg -------------------------------------------------------------------------------- /src/main/resources/static/image/millky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origoni/spring-blog/ad7748d4fa85b3cd30cea2abe2fbc82051829b36/src/main/resources/static/image/millky.png -------------------------------------------------------------------------------- /src/main/resources/static/image/post-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origoni/spring-blog/ad7748d4fa85b3cd30cea2abe2fbc82051829b36/src/main/resources/static/image/post-bg.jpg -------------------------------------------------------------------------------- /src/main/resources/static/image/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origoni/spring-blog/ad7748d4fa85b3cd30cea2abe2fbc82051829b36/src/main/resources/static/image/top.png -------------------------------------------------------------------------------- /src/main/resources/templates/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello Millky 6 | 7 | 8 | 9 |

Hello! ${name}

10 |
Thymeleaf version
11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/templates/hello.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello Millky 6 | 7 | 8 | ## 9 | <h2>Hello! ${name}</h2> 10 | <div>Velocity version</div> 11 | </body> 12 | </html> -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/connect/login.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <!DOCTYPE html> 4 | <html lang="ko"> 5 | <head> 6 | <%@ include file="/WEB-INF/jspf/head.jspf" %> 7 | <title>Login</title> 8 | </head> 9 | <body> 10 | <%@ include file="/WEB-INF/jspf/nav.jspf" %> 11 | 12 | <header class="intro-header" style="background-image: url('/image/contact-bg.jpg')"> 13 | <div class="container"> 14 | <div class="row"> 15 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 16 | <div class="page-heading"> 17 | <h1>Login</h1> 18 | <hr class="small"> 19 | <span class="subheading">Connect to ...</span> 20 | </div> 21 | </div> 22 | </div> 23 | </div> 24 | </header> 25 | 26 | <div class="container"> 27 | <h3>Login with OAuth 2.0</h3> 28 | 29 | <table> 30 | <c:forEach var="url" items="${urls}"> 31 | <tr><td><a class="btn-warning" href="${url.value}">${url.key}</a></td></tr> 32 | </c:forEach> 33 | </table> 34 | </div> 35 | 36 | <%@ include file="/WEB-INF/jspf/footer.jspf" %> 37 | </body> 38 | </html> -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/error.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <!DOCTYPE html> 3 | <html lang="ko"> 4 | <head> 5 | <%@ include file="/WEB-INF/jspf/head.jspf"%> 6 | <title>${status}Error</title> 7 | </head> 8 | <body> 9 | <%@ include file="/WEB-INF/jspf/nav.jspf"%> 10 | 11 | <header class="intro-header" style="background-image: url('/image/about-bg.jpg')"> 12 | <div class="container"> 13 | <div class="row"> 14 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 15 | <div class="site-heading"> 16 | <h1>Error</h1> 17 | </div> 18 | </div> 19 | </div> 20 | </div> 21 | </header> 22 | 23 | <div class="container"> 24 | <div class="row"> 25 | <div class="col-lg-12"> 26 | <h1>${status} Error</h1> 27 | 28 | <h3> 29 | <i class="fa fa-warning"></i> ${error} 30 | </h3> 31 | 32 | <p>${message}</p> 33 | </div> 34 | </div> 35 | </div> 36 | <%@ include file="/WEB-INF/jspf/footer.jspf"%> 37 | </body> 38 | </html> 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/hello.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 4 | <!DOCTYPE html> 5 | <html lang="ko"> 6 | <head> 7 | <%@ include file="/WEB-INF/jspf/head.jspf"%> 8 | <title>Hello Millky</title> 9 | </head> 10 | <body> 11 | <a href="https://github.com/origoni/Spring-Blog"><img style="position: absolute; top: 0; right: 0; border: 0; z-index: 2;" src="https://camo.githubusercontent.com/652c5b9acfaddf3a9c326fa6bde407b87f7be0f4/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6f72616e67655f6666373630302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png"></a> 12 | <div class="container" id="body_test"> 13 | <h2>Hello! ${name}</h2> 14 | <div>JSP version</div> 15 | 16 | <a href="/post/list"> 17 | <button type="button" class="btn btn-lg btn-success btn-block"> 18 | <c:if test="${_USER!=null}">환영합니다! ${_USER.displayName}<br><br></c:if> 19 | Spring Blog 라이브 데모 들어가기 20 | </button> 21 | </a> 22 | 23 | <article> 24 | <h1>SpringBlog from Millky</h1> 25 | <p> 26 | 밀키(millky.com)에서 블로그 부분을 뽑아(?) 오픈소스 합니다. <a href="http://millky.com/@origoni">http://millky.com/@origoni</a><br> 27 | 단지 코드만 공개한는 것이 아니라. 개발 과정을 같이 공개하려 합니다.<br> 문의사항은 밀키, 페이스북, 깃헙 등 모두 열려 있습니다 ^^; 28 | </p> 29 | 30 | <h3>관련 링크</h3> 31 | <ul> 32 | <li><a href="http://millky.com/">http://millky.com/</a></li> 33 | <li><a href="http://millky.com/@origoni/folder/30/post/list">http://millky.com/@origoni/folder/30/post/list</a></li> 34 | <li><a href="https://github.com/origoni/Spring-Blog">https://github.com/origoni/Spring-Blog</a></li> 35 | </ul> 36 | 37 | <h3>Project Convention</h3> 38 | 39 | <h4>Package Structure</h4> 40 | <pre><code>com.millky.blog 41 | └── application 42 | └── configuration 43 | └── utility 44 | └── domain 45 | └── model 46 | └── command 47 | └── entity 48 | └── exception 49 | └── repository 50 | └── service 51 | └── infrastructure 52 | └── dao 53 | └── presentation 54 | └── controller 55 | └── rest 56 | └── support 57 | └── result</code></pre> 58 | 59 | <h4>프로젝트 설정</h4> 60 | 61 | <ol> 62 | <li>STS 설치 -&gt; <a href="http://millky.com/@origoni/post/1100">http://millky.com/@origoni/post/1100</a></li> 63 | <li>Lombok 설치 -&gt; <a href="https://projectlombok.org/">https://projectlombok.org/</a> 64 | (STS에 설치 : <a href="http://millky.com/@origoni/post/1164">http://millky.com/@origoni/post/1164</a>) 65 | </li> 66 | <li>GitHub 에서 다운 -&gt; <a href="http://millky.com/@origoni/post/1145">http://millky.com/@origoni/post/1145</a> 67 | (OSX : <a href="http://millky.com/@origoni/post/1140">http://millky.com/@origoni/post/1140</a>) 68 | </li> 69 | </ol> 70 | </article> 71 | 72 | <div>fmt:message Test : <fmt:message key="required"/></div> 73 | </div> 74 | 75 | <%@ include file="/WEB-INF/jspf/footer.jspf"%> 76 | </body> 77 | </html> 78 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/post/form.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> 4 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 5 | <!DOCTYPE html> 6 | <html lang="ko"> 7 | <head> 8 | <meta charset="utf-8"> 9 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 10 | <meta name="viewport" content="width=device-width, initial-scale=1"> 11 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 12 | <title>Pen - What You See Is What You Get (WYSIWYG)</title> 13 | <link rel="stylesheet" href="/webjars/bootstrap/dist/css/bootstrap.min.css"> 14 | <style type="text/css"> 15 | *{padding:0;margin:0;} 16 | html{border-top:10px #1abf89 solid;} 17 | body{width:750px;margin:0 auto;padding:7% 20px 20px;} 18 | @media all and (max-width:1024px){ body, pre a{width:85%;} } 19 | @media (max-width: 767px) { body, pre a{width:95%;} body{margin:0 auto;padding:50px 5px 5px;} } 20 | small{color:#999;} 21 | #toolbar [class^="icon-"]:before, #toolbar [class*=" icon-"]:before{font-family:'pen'} 22 | @media (max-width: 767px) { #toolbar{margin-bottom:1em;position:static;right:auto;margin-top:25px;width:100%;} } 23 | @media (min-width: 768px) { #toolbar{margin-bottom:1em;position:fixed;right:20px;top:25px;} } 24 | #back{color:#1abf89;cursor:pointer;} 25 | #hinted{color:#1abf89;cursor:pointer;} 26 | #hinted.disabled{color:#666;} 27 | #hinted:before{content: '\e816';} 28 | .error {color: red;} 29 | </style> 30 | 31 | <link rel="stylesheet" href="/webjars/pen/src/pen.css" /> 32 | <link rel="stylesheet" href="/webjars/origoni-startbootstrap-clean-blog/css/clean-blog.min.css" /> 33 | 34 | <style type="text/css"> 35 | .pen-icon {padding: 0 9.3px;} 36 | .pen-menu {opacity: 0.8; border: -1px; height: 37px;} 37 | .pen-menu:after {display: none;} 38 | .pen p {font-family: Lora, 'Times New Roman', serif; font-size: 20px; color: #404040;} 39 | .pen h1, h2, h3, h4, h5, h6 { 40 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 41 | font-weight: 800; 42 | margin-top: 20px; 43 | margin-bottom: 10px; 44 | line-height: 1.1; 45 | } 46 | .pen h1 {font-size: 36px;} 47 | .pen h2 {font-size: 30px;} 48 | .pen h3 {font-size: 24px;} 49 | .pen h4 {font-size: 18px;} 50 | </style> 51 | </head> 52 | <body> 53 | 54 | <div id="custom-toolbar" class="pen-menu pen-menu" style="display: block; top: 20px; margin:0 auto;"> 55 | <%--<i class="pen-icon icon-insertimage" data-action="insertimage"></i>--%> 56 | <i class="pen-icon icon-blockquote" data-action="blockquote"></i> 57 | <i class="pen-icon icon-h1" data-action="h1"></i> 58 | <i class="pen-icon icon-h2" data-action="h2"></i> 59 | <i class="pen-icon icon-h3" data-action="h3"></i> 60 | <i class="pen-icon icon-p active" data-action="p"></i> 61 | <i class="pen-icon icon-code" data-action="code"></i> 62 | <i class="pen-icon icon-insertorderedlist" data-action="insertorderedlist"></i> 63 | <i class="pen-icon icon-insertunorderedlist" data-action="insertunorderedlist"></i> 64 | <i class="pen-icon icon-inserthorizontalrule" data-action="inserthorizontalrule"></i> 65 | <i class="pen-icon icon-indent" data-action="indent"></i> 66 | <i class="pen-icon icon-outdent" data-action="outdent"></i> 67 | <i class="pen-icon icon-bold" data-action="bold"></i> 68 | <i class="pen-icon icon-italic" data-action="italic"></i> 69 | <i class="pen-icon icon-underline" data-action="underline"></i> 70 | <i class="pen-icon icon-createlink" data-action="createlink"></i> 71 | </div> 72 | 73 | <form:form action="${requestScope['javax.servlet.forward.servlet_path']}" id="post" modelAttribute="postCommand" onsubmit="if($('#pen').html()!='<p><br></p>')$('#content').val($('#pen').html()); pen.destroy();" method="post"> 74 | <%--<form:form action="${requestScope['javax.servlet.forward.servlet_path']}" commandName="postCommand" id="post" onsubmit="if($('#pen').html()!='<p><br></p>')$('#content').val($('#pen').html()); pen.destroy();" method="post">--%> 75 | 76 | <form:input type="hidden" path="_csrf" value="${_csrf.token}"></form:input> 77 | 78 | <form:input type="text" path="title" placeholder="Title" 79 | style="height: 70px; width: 100%; font-size: 55px; 80 | border: none; border-right: 0px; border-top: 0px; boder-left: 0px; boder-bottom: 1px; outline-style: none; 81 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 800;" /> 82 | <form:errors path="title" cssClass="error" /> 83 | 84 | <form:input type="text" path="subtitle" placeholder="Subtitle (option)" 85 | style="height: 60px; width: 100%; font-size: 24px; 86 | border: none; border-right: 0px; border-top: 0px; boder-left: 0px; boder-bottom: 1px; outline-style: none; 87 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 600;" /> 88 | 89 | <hr style="margin-top: 2px; border-top: 1px solid #999;"> 90 | 91 | <div data-toggle="pen" data-placeholder="Content" id="pen" style="min-height: 200px;"></div> 92 | <form:input type="hidden" path="content" id="content" /> 93 | <form:errors path="content" cssClass="error" /> 94 | 95 | <form:input type="text" path="tags" placeholder="Tag (option - 최대 10개. 공백으로 구분합니다.)" 96 | style="height: 40px; width: 100%; font-size: 18px; 97 | border: none; border-right: 0px; border-top: 0px; boder-left: 0px; boder-bottom: 1px; outline-style: none; 98 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 600;" /> 99 | 100 | <hr style="margin-top: 2px; border-top: 1px solid #999;"> 101 | 102 | <div class="form-group" style="height: 30px;"> 103 | <label for="category" class="col-sm-2 col-xs-3 control-label" style="padding-left: 5px;">Category</label> 104 | <div class="col-sm-10 col-xs-9" style="padding-right: 5px;"> 105 | <form:select path="categoryId" items="${categoryMap}" id="category" class="form-control"/> 106 | <form:errors path="categoryId" cssClass="error" /> 107 | </div> 108 | </div> 109 | 110 | <button type="submit" class="btn btn-primary btn-lg btn-block">저장</button> 111 | 112 | </form:form> 113 | 114 | 115 | <div id="toolbar"> 116 | <span id="back" class="icon-back" onclick="history.back();">돌아가기</span><br> 117 | <span id="hinted" class="icon-pre disabled" title="Toggle Markdown Hints"></span> 118 | 119 | <form action="/category/add" method="post" id="add_category" > 120 | <input type="text" name="categoryName" class="form-control" placeholder="새로운 카테고리" required="required"> 121 | <input type="hidden" name="_csrf" value="${_csrf.token}"> 122 | <button type="submit" class="form-control">추가</button> 123 | </form> 124 | </div> 125 | 126 | <p class="text-muted" style="font-size: 14px;">Powered By <a href="http://millky.com">Millky</a> | WYSIWYG Editor by <a href="https://github.com/sofish/pen">Pen Editor</a></p> 127 | 128 | <script src="/webjars/jquery/dist/jquery.min.js"></script> 129 | <script src="/webjars/bootstrap/dist/js/bootstrap.min.js"></script> 130 | <script src="/webjars/pen/src/pen.js"></script> 131 | <script src="/webjars/pen/src/markdown.js"></script> 132 | <script type="text/javascript"> 133 | $('#add_category').submit(function(event) { 134 | var form = $(this); 135 | 136 | $.ajax({ 137 | type : form.attr('method'), 138 | url : form.attr('action'), 139 | data : form.serialize() 140 | }).done(function(c) { 141 | $("#category").append("<option value=" + c.id + ">" + c.name + "</option>"); 142 | $("#category").val(c.id); 143 | 144 | alert(c.name + " 카테고리가 추가되었습니다."); 145 | }).fail(function() { 146 | alert('error'); 147 | }); 148 | event.preventDefault(); 149 | }); 150 | 151 | // config 152 | var options = { 153 | toolbar : document.getElementById('custom-toolbar'), 154 | editor : document.querySelector('[data-toggle="pen"]') 155 | }; 156 | 157 | $('#pen').html($('#content').val()); 158 | 159 | // create editor 160 | var pen = window.pen = new Pen(options); 161 | 162 | pen.focus(); 163 | 164 | document.querySelector('#hinted').addEventListener('click', function() { 165 | var pen = document.querySelector('.pen') 166 | 167 | if (pen.classList.contains('hinted')) { 168 | pen.classList.remove('hinted'); 169 | this.classList.add('disabled'); 170 | } else { 171 | pen.classList.add('hinted'); 172 | this.classList.remove('disabled'); 173 | } 174 | }); 175 | </script> 176 | </body> 177 | </html> -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/post/list.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <!DOCTYPE html> 4 | <html lang="ko"> 5 | <head> 6 | <%@ include file="/WEB-INF/jspf/head.jspf" %> 7 | <title>Hello Spring Blog</title> 8 | </head> 9 | <body> 10 | <%@ include file="/WEB-INF/jspf/nav.jspf" %> 11 | 12 | <header class="intro-header" style="background-image: url('/image/home-bg.jpg')"> 13 | <div class="container"> 14 | <div class="row"> 15 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 16 | <div class="site-heading"> 17 | <h1>Spring Blog</h1> 18 | <hr class="small"> 19 | <span class="subheading">Spring Blog form Millky</span> 20 | </div> 21 | </div> 22 | </div> 23 | </div> 24 | </header> 25 | 26 | <div class="container"> 27 | <div class="row"> 28 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 29 | <c:if test="${query!=null}"> 30 | <c:out value="${query}" escapeXml="true" /> (으)로 검색된 31 | </c:if> 32 | <c:if test="${category!=null}"> 33 | <c:out value="${category}" escapeXml="true" /> 카테고리에 34 | </c:if> 35 | <c:if test="${tag!=null}"> 36 | <c:out value="${tag}" escapeXml="true" /> 태그에 37 | </c:if> 38 | <c:if test="${postPage.totalElements>0}"> 39 | 총 ${postPage.totalElements} 개의 글이 있습니다. 40 | </c:if> 41 | <c:if test="${postPage.totalElements==0}"> 42 | 글이 없습니다. 43 | </c:if> 44 | <c:forEach var="post" items="${postPage.content}"> 45 | <div class="post-preview"> 46 | <a href="/post/${post.id}"> 47 | <h2 class="post-title"> 48 | <c:out value="${post.title}" escapeXml="true"></c:out> 49 | </h2> 50 | <h3 class="post-subtitle"> 51 | <c:out value="${post.subtitle}" escapeXml="true"></c:out> 52 | </h3> 53 | </a> 54 | <p class="post-meta">Posted by <a href="#">${post.name}</a> in <a href="/category/${post.category.id}/post/list"><c:out value="${post.category.name}" escapeXml="true" /></a> on ${post.regDate}</p> 55 | </div> 56 | <hr> 57 | </c:forEach> 58 | 59 | <ul class="pager"> 60 | <c:if test="${!postPage.first}"> 61 | <li class="previous"> 62 | <c:if test="${query==null}"> 63 | <a href="?page=${postPage.number-1}">&larr; Newer Posts</a> 64 | </c:if> 65 | <c:if test="${query!=null}"> 66 | <a href="?type=${param.type}&query=${param.query}&page=${postPage.number-1}">&larr; Newer Posts</a> 67 | </c:if> 68 | </li> 69 | </c:if> 70 | <c:if test="${!postPage.last}"> 71 | <li class="next"> 72 | <c:if test="${query==null}"> 73 | <a href="?page=${postPage.number+1}">Older Posts &rarr;</a> 74 | </c:if> 75 | <c:if test="${query!=null}"> 76 | <a href="?type=${param.type}&query=${param.query}&page=${postPage.number+1}">Older Posts &rarr;</a> 77 | </c:if> 78 | </li> 79 | </c:if> 80 | </ul> 81 | 82 | <br /> 83 | 84 | <form action="/post/search" method="get" class="form-inline"> 85 | <select class="form-control input-lg" name="type"> 86 | <option <c:if test="${type=='title'}">selected="selected" </c:if>value="title">Title</option> 87 | <option <c:if test="${type=='contents'}">selected="selected" </c:if>value="contents">Contents</option> 88 | <option <c:if test="${type=='titleAndContents'}">selected="selected" </c:if>value="titleAndContents">Title And Contents</option> 89 | </select> 90 | <div class="input-group"> 91 | <input type="text" name="query" class="form-control input-lg" placeholder="Search..." value="${query}"> 92 | <span class="input-group-btn"> 93 | <button type="submit" class="btn btn-default input-lg" style="border-radius: 0 4px 4px 0; padding: 10px 20px;"> 94 | <i class="fa fa-search"></i> 95 | </button> 96 | </span> 97 | </div> 98 | </form> 99 | 100 | </div> 101 | </div> 102 | </div> 103 | <hr> 104 | <%@ include file="/WEB-INF/jspf/footer.jspf" %> 105 | </body> 106 | </html> 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/post/post.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> 4 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 5 | <!DOCTYPE html> 6 | <html lang="${pageContext.request.locale.language}"> 7 | <head> 8 | <%@ include file="/WEB-INF/jspf/head.jspf" %> 9 | <title><c:out value="${post.title}" escapeXml="true" /> : Spring Blog</title> 10 | </head> 11 | <body> 12 | <%@ include file="/WEB-INF/jspf/nav.jspf" %> 13 | 14 | <header class="intro-header" style="background-image: url('/image/post-bg.jpg')"> 15 | <div class="container"> 16 | <div class="row"> 17 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 18 | <div class="post-heading"> 19 | <h1><c:out value="${post.title}" escapeXml="true"></c:out></h1> 20 | <h2 class="subheading"><c:out value="${post.subtitle}" escapeXml="true" /></h2> 21 | <span class="meta">Posted by <a href="#">${post.name}</a> in <a href="/category/${post.category.id}/post/list"><c:out value="${post.category.name}" escapeXml="true" /></a> on ${post.regDate}</span> 22 | </div> 23 | </div> 24 | </div> 25 | </div> 26 | </header> 27 | 28 | <article> 29 | <div class="container"> 30 | <div class="row"> 31 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 32 | ${post.content} 33 | </div> 34 | 35 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 36 | <h3 style="line-height: 1.6;"> 37 | <c:forEach var="postTag" items="${post.postTagList}" varStatus="status"> 38 | <a href="/tag/<c:out value="${postTag.tag.name}" escapeXml="true" />/post/list"> 39 | <span class="label label-default"><c:out value="${postTag.tag.name}" escapeXml="true" /></span></a> 40 | </c:forEach> 41 | </h3> 42 | </div> 43 | </div> 44 | 45 | <c:if test="${_USER!=null && _USER.providerUserId == post.userId}"> 46 | <br> 47 | <div class="pull-right"> 48 | <a href="/post/${post.id}/edit"> 49 | <button type="button" class="btn btn-warning">Edit</button> 50 | </a> 51 | <a href="/post/${post.id}/delete" onclick="if(!confirm('진심이에요?')){return false;}"> 52 | <button type="button" class="btn btn-danger">Delete</button> 53 | </a> 54 | </div> 55 | </c:if> 56 | 57 | <hr> 58 | <div class="row"> 59 | <div id="target" class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"></div> 60 | <c:if test="${_USER!=null}"> 61 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 62 | <br> 63 | <form action="/comments" method="post" id="comment_form"> 64 | <input type="hidden" name="postId" value="${post.id}"> 65 | <input type="hidden" name="_csrf" value="${_csrf.token}"></input> 66 | <div class="media"> 67 | <div class="media-body"> 68 | <textarea name="content" class="form-control" rows="2"></textarea> 69 | </div> 70 | <div class="media-right"> 71 | <button class="btn" type="submit">저장</button> 72 | </div> 73 | </div> 74 | </form> 75 | </div> 76 | </c:if> 77 | </div> 78 | </div> 79 | </article> 80 | 81 | <hr> 82 | 83 | <%@ include file="/WEB-INF/jspf/footer.jspf" %> 84 | 85 | <script id="template" type="x-tmpl-mustache"> 86 | {{#.}} 87 | <div class="media"> 88 | <div class="media-body"> 89 | {{{content}}}<br> 90 | <h4 class="media-heading" style="display: inline-block;">{{name}}</h4> on {{momentNow}} <small>({{momentDate}})</small> 91 | {{#myComment}}<button type="button" style="margin-bottom: 5px;" class="btn btn-danger btn-sm" onclick="if(!confirm('진심이에요?')){return false;} deleteComment({{postId}}, {{id}});">Delete</button>{{/myComment}} 92 | <br> 93 | </div> 94 | </div> 95 | {{/.}} 96 | </script> 97 | 98 | <script type="text/javascript"> 99 | 100 | function deleteComment(postId, commentId) { 101 | $.ajax({ 102 | type : "delete", 103 | url : "/comments/" + commentId + "?postId=" + postId, 104 | dataType : 'json', 105 | beforeSend : function(xhr) { 106 | xhr.setRequestHeader('X-CSRF-Token', '${_csrf.token}'); 107 | }, 108 | success : function(data, status) { 109 | loadComment(); 110 | }, 111 | error : function(data, status) { 112 | alert(data.responseJSON.message); 113 | } 114 | }); 115 | } 116 | 117 | $("#comment_form").submit(function(event) { 118 | var form = $(this); 119 | $.ajax({ 120 | type : form.attr('method'), 121 | url : form.attr('action'), 122 | data : form.serialize(), 123 | dataType : 'json', 124 | success : function(data, status) { 125 | loadComment(); 126 | form[0].reset(); 127 | }, 128 | error : function(data, status) { 129 | alert(data.responseJSON.message); 130 | } 131 | }); 132 | event.preventDefault(); 133 | }); 134 | 135 | var autolinker = new Autolinker( { 136 | newWindow : false, 137 | truncate : 70 138 | } ); 139 | 140 | moment.locale('${pageContext.request.locale.language}'); 141 | var template = $('#template').html(); 142 | Mustache.parse(template); 143 | function loadComment() { 144 | $.ajax({ 145 | type : "GET", 146 | url : "/comments", 147 | data : "postId=${post.id}", 148 | dataType : 'json', 149 | cache : false, 150 | success : function(data, status) { 151 | 152 | for (k in data) { 153 | object = data[k]; 154 | // console.log("object = " + object); 155 | for (key in object) { 156 | value = object[key]; 157 | if (key == "regDate") { 158 | object['momentDate'] = moment(value).format("YYYY-MM-DD HH:mm:ss"); 159 | object['momentNow'] = moment(value).fromNow(); 160 | } 161 | if (key == "content") { 162 | object['content'] = autolinker.link(value).replace(/(?:\r\n|\r|\n)/g, "<br />"); // /\r?\n|\r/g 163 | } 164 | /* <c:if test="${_USER!=null}"> */ 165 | if (key == "userId") { 166 | // console.log("value = " + value); 167 | if (value == "${_USER.providerUserId}") { 168 | object['myComment'] = true; 169 | } 170 | } 171 | /* </c:if> */ 172 | } 173 | } 174 | // console.log(data); 175 | $('#target').html(Mustache.render(template, data)); 176 | }, 177 | error : function(data, status) { 178 | alert("error"); 179 | } 180 | }).always(function() { 181 | }); 182 | } 183 | 184 | loadComment(); 185 | </script> 186 | 187 | </body> 188 | </html> 189 | 190 | 191 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/post/rss.jsp: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <%@ page contentType="text/xml;charset=UTF-8" pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 5 | <rss version="2.0"> 6 | <channel> 7 | <title><![CDATA[Spring blog]]></title> 8 | <link>http://blog.millky.com/post/list</link> 9 | <description><![CDATA[아카이브, 블로그, 커뮤니티 서비스]]></description> 10 | <language>ko-kr</language> 11 | <generator>https://github.com/origoni/Spring-Blog</generator> 12 | <c:forEach var="post" items="${postList}"> 13 | <item> 14 | <title><![CDATA[<c:out value="${post.title}" escapeXml="true" />]]></title> 15 | <link>http://blog.millky.com/post/${post.id}</link> 16 | <description> 17 | <c:if test="${post.subtitle!=''}"><![CDATA[<c:out value="${post.subtitle}" escapeXml="true" />]]></c:if> 18 | <![CDATA[${post.content}]]> 19 | </description> 20 | <category><![CDATA[<c:out value="${post.category.name}" escapeXml="true" />]]></category> 21 | <author><![CDATA[<c:out value="${post.name}" escapeXml="true" />]]></author> 22 | <guid>http://blog.millky.com/post/${post.id}</guid> 23 | <pubDate><fmt:formatDate value="${post.regDate}" pattern="EE, dd MMM yyyy HH:mm:ss Z"/></pubDate> 24 | </item> 25 | </c:forEach> 26 | </channel> 27 | </rss> -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jspf/footer.jspf: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <footer> 3 | <div class="container"> 4 | <div class="row"> 5 | <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1"> 6 | <ul class="list-inline text-center"> 7 | <li> 8 | <a href="https://www.facebook.com/millkyc"> 9 | <span class="fa-stack fa-lg"> 10 | <i class="fa fa-circle fa-stack-2x"></i> 11 | <i class="fa fa-facebook fa-stack-1x fa-inverse"></i> 12 | </span> 13 | </a> 14 | </li> 15 | <li> 16 | <a href="https://github.com/origoni"> 17 | <span class="fa-stack fa-lg"> 18 | <i class="fa fa-circle fa-stack-2x"></i> 19 | <i class="fa fa-github fa-stack-1x fa-inverse"></i> 20 | </span> 21 | </a> 22 | </li> 23 | <li> 24 | <a href="https://twitter.com/origoni"> 25 | <span class="fa-stack fa-lg"> 26 | <i class="fa fa-circle fa-stack-2x"></i> 27 | <i class="fa fa-twitter fa-stack-1x fa-inverse"></i> 28 | </span> 29 | </a> 30 | </li> 31 | </ul> 32 | <p class="copyright text-muted"> 33 | Powered By <a href="http://millky.com">Millky</a> 34 | | Skin designed by <a href="http://startbootstrap.com/template-overviews/clean-blog/">Start Bootstrap</a> 35 | </p> 36 | </div> 37 | </div> 38 | </div> 39 | </footer> 40 | 41 | <div> 42 | <span class="backToTop" id="backToTop"><a href="#top">back to top</a></span> 43 | </div> 44 | 45 | <script src="/webjars/jquery/dist/jquery.min.js"></script> 46 | <script src="/webjars/bootstrap/dist/js/bootstrap.min.js"></script> 47 | <script src="/webjars/jqcloud2/dist/jqcloud.min.js"></script> 48 | <script src="/webjars/mustache/mustache.min.js"></script> 49 | <script src="/webjars/momentjs/moment.js"></script> 50 | <script src="/webjars/momentjs/locale/${pageContext.request.locale.language}.js"></script> 51 | <script src="/webjars/Autolinker.js/dist/Autolinker.min.js"></script> 52 | 53 | <script type="text/javascript"> 54 | // Closes the sidebar menu 55 | $("#menu-close").click(function(e) { 56 | e.preventDefault(); 57 | $("#sidebar-wrapper").removeClass("active"); 58 | }); 59 | 60 | // Opens the sidebar menu 61 | var loadTag = false; 62 | $("#menu-toggle").click(function(e) { 63 | e.preventDefault(); 64 | 65 | if (loadTag == false) { 66 | $.ajax({ 67 | url : '/api/v1/tag-cloud', 68 | dataType : 'json', 69 | success : function(word_array) { 70 | $("#tag-cloud").jQCloud(word_array); 71 | loadTag = true; 72 | } 73 | }) 74 | } 75 | 76 | $("#sidebar-wrapper").toggleClass("active"); 77 | }); 78 | 79 | // hide .backToTop first 80 | $(".backToTop").hide(); 81 | $(window).scroll(function() { 82 | if ($(this).scrollTop() > 100) { 83 | $('.backToTop').fadeIn(); 84 | } else { 85 | $('.backToTop').fadeOut(); 86 | } 87 | }); 88 | 89 | $('.backToTop').click(function() { 90 | $("html, body").animate({ 91 | scrollTop : 0 92 | }, 300); 93 | return false; 94 | }); 95 | </script> 96 | 97 | <script> 98 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 99 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 100 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 101 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 102 | 103 | ga('create', 'UA-23619051-4', 'auto'); 104 | ga('send', 'pageview'); 105 | 106 | </script> 107 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jspf/head.jspf: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <meta charset="utf-8"> 3 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 4 | <meta name="viewport" content="width=device-width, initial-scale=1"> 5 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 6 | <link rel="stylesheet" href="/webjars/bootstrap/dist/css/bootstrap.min.css"> 7 | <link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css"> 8 | <link rel="stylesheet" href="/webjars/origoni-startbootstrap-clean-blog/css/clean-blog.min.css"> 9 | <link rel="stylesheet" href="/webjars/jqcloud2/dist/jqcloud.min.css"> 10 | <link rel="alternate" type="application/rss+xml" title="Spring Blog (RSS 2.0)" href="http://blog.millky.com/rss/post/list" /> 11 | 12 | <style type="text/css"> 13 | 14 | .btn-group-xs>.btn, .btn-xs { 15 | padding: 1px 5px; 16 | font-size: 12px; 17 | line-height: 1.5; 18 | border-radius: 3px; 19 | } 20 | 21 | .btn-group-sm>.btn, .btn-sm { 22 | padding: 5px 10px; 23 | font-size: 12px; 24 | line-height: 1.5; 25 | border-radius: 3px; 26 | } 27 | 28 | /* Custom Button Styles */ 29 | .btn-dark { 30 | border-radius: 0; 31 | color: #fff; 32 | background-color: rgba(0, 0, 0, 0.4); 33 | padding: 10px 16px; 34 | font-size: 18px; 35 | line-height: 1.3333333; 36 | } 37 | 38 | .btn-dark:hover, .btn-dark:focus, .btn-dark:active { 39 | color: #fff; 40 | background-color: rgba(0, 0, 0, 0.7); 41 | } 42 | 43 | .btn-light { 44 | border-radius: 0; 45 | color: #333; 46 | padding: 10px 20px 10px 0; 47 | font-size: 18px; 48 | line-height: 1.3333333; 49 | } 50 | 51 | /* Side Menu */ 52 | #sidebar-wrapper { 53 | z-index: 1000; 54 | position: fixed; 55 | right: 0; 56 | width: 250px; 57 | height: 100%; 58 | margin-right: -250px; 59 | overflow-y: auto; 60 | background: #222; 61 | -webkit-transition: all 0.4s ease 0s; 62 | -moz-transition: all 0.4s ease 0s; 63 | -ms-transition: all 0.4s ease 0s; 64 | -o-transition: all 0.4s ease 0s; 65 | transition: all 0.4s ease 0s; 66 | } 67 | 68 | .sidebar-nav { 69 | position: absolute; 70 | top: 0; 71 | width: 250px; 72 | margin: 0; 73 | padding: 0; 74 | list-style: none; 75 | } 76 | 77 | .sidebar-nav li { 78 | text-indent: 20px; 79 | line-height: 40px; 80 | } 81 | 82 | .sidebar-nav li a { 83 | display: block; 84 | text-decoration: none; 85 | color: #999; 86 | } 87 | 88 | .sidebar-nav li a:hover { 89 | text-decoration: none; 90 | color: #fff; 91 | background: rgba(255, 255, 255, 0.2); 92 | } 93 | 94 | .sidebar-nav li a:active, .sidebar-nav li a:focus { 95 | text-decoration: none; 96 | } 97 | 98 | .sidebar-nav>.sidebar-brand { 99 | height: 55px; 100 | font-size: 18px; 101 | line-height: 55px; 102 | } 103 | 104 | .sidebar-nav>.sidebar-brand a { 105 | color: #999; 106 | } 107 | 108 | .sidebar-nav>.sidebar-brand a:hover { 109 | color: #fff; 110 | background: none; 111 | } 112 | 113 | .sidebar-nav .divider { 114 | height: 1px; 115 | margin: 9px 0; 116 | overflow: hidden; 117 | background-color: #444; 118 | } 119 | 120 | #menu-toggle { 121 | z-index: 1; 122 | position: fixed; 123 | top: 5px; 124 | right: 5px; 125 | color: #fff; 126 | } 127 | 128 | #menu-home { 129 | z-index: 1; 130 | position: fixed; 131 | top: 5px; 132 | right: 60px; 133 | color: #fff; 134 | } 135 | 136 | #sidebar-wrapper.active { 137 | right: 250px; 138 | width: 250px; 139 | -webkit-transition: all 0.4s ease 0s; 140 | -moz-transition: all 0.4s ease 0s; 141 | -ms-transition: all 0.4s ease 0s; 142 | -o-transition: all 0.4s ease 0s; 143 | transition: all 0.4s ease 0s; 144 | } 145 | 146 | .toggle { 147 | top: 5px; 148 | right: 0px; 149 | position: absolute; 150 | } 151 | 152 | 153 | /* go to top 154 | ================================================== */ 155 | .backToTop { 156 | position: fixed; 157 | bottom: 10px; 158 | z-index: 1000; 159 | right: 10px; 160 | } 161 | 162 | .backToTop a { 163 | opacity: .4; 164 | width: 50px; 165 | text-indent: -9999px; 166 | height: 50px; 167 | display: block; 168 | margin: 0; 169 | background: #53a3b7 url(/image/top.png) no-repeat center center; 170 | } 171 | 172 | .backToTop a:hover { 173 | opacity: .8; 174 | } 175 | 176 | /* go to top 177 | ================================================== */ 178 | </style> -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jspf/nav.jspf: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <a href="https://github.com/origoni/Spring-Blog"><img style="position: absolute; top: 0; left: 0; border: 0; z-index: 5;" src="https://camo.githubusercontent.com/8b6b8ccc6da3aa5722903da7b58eb5ab1081adee/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f6c6566745f6f72616e67655f6666373630302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_left_orange_ff7600.png"></a> 4 | 5 | <a id="menu-home" href="/post/list" class="btn btn-dark">Home</a> 6 | <a id="menu-toggle" href="#" class="btn btn-dark"><i class="fa fa-bars"></i></a> 7 | <nav id="sidebar-wrapper"> 8 | <ul class="sidebar-nav"> 9 | <li class="sidebar-brand"> 10 | <a href="/post/list">SpringBlog</a> 11 | <a id="menu-close" href="#" class="btn btn-light pull-right toggle"> 12 | <i class="fa fa-times"></i> 13 | </a> 14 | </li> 15 | <li role="separator" class="divider"></li> 16 | <li class="sidebar-brand"> 17 | <a href="/post/write">Write Post</a> 18 | </li> 19 | <li class="sidebar-brand"> 20 | <c:if test="${_USER==null}"> 21 | <a href="/user/login">Login</a> 22 | </c:if> 23 | <c:if test="${_USER!=null}"> 24 | <form action="/user/logout" method="post"> 25 | <button type="submit" class="btn">Disconnect</button> 26 | <input type="hidden" name="_csrf" value="${_csrf.token}"></input> 27 | </form> 28 | </c:if> 29 | </li> 30 | <li role="separator" class="divider"></li> 31 | <li class="sidebar-brand"> 32 | Category 33 | </li> 34 | <c:forEach items="${_CATEGORY_LIST}" var="category"> 35 | <li> 36 | <a href="/category/${category.id}/post/list"><c:out value="${category.name}" escapeXml="true" /></a> 37 | </li> 38 | </c:forEach> 39 | <li role="separator" class="divider"></li> 40 | <li class="sidebar-brand"> 41 | Tags 42 | </li> 43 | <div id="tag-cloud" style="width: 95%; height: 270px; margin: -10px 5px;"></div> 44 | <li role="separator" class="divider"></li> 45 | <li class="sidebar-brand"> 46 | <a href="/swagger-ui/index.html">APIs</a> 47 | <%-- <a href="/swagger-ui.html">APIs</a>--%> 48 | </li> 49 | <li> 50 | <a href="/swagger-ui/index.html#/tag-rest-controller/tagCloud">- tagCloudUsingGET</a> 51 | </li> 52 | <li role="separator" class="divider"></li> 53 | <li class="sidebar-brand"> 54 | <a href="/">Root</a> 55 | </li> 56 | </ul> 57 | </nav> 58 | -------------------------------------------------------------------------------- /src/test/java/com/millky/blog/SpringBlogApplicationTests.java: -------------------------------------------------------------------------------- 1 | //package com.millky.blog; 2 | // 3 | //import org.junit.Test; 4 | //import org.junit.runner.RunWith; 5 | //import org.springframework.test.context.web.WebAppConfiguration; 6 | //import org.springframework.boot.test.SpringApplicationConfiguration; 7 | //import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | // 9 | //@RunWith(SpringJUnit4ClassRunner.class) 10 | //@SpringApplicationConfiguration(classes = SpringBlogApplication.class) 11 | //@WebAppConfiguration 12 | //public class SpringBlogApplicationTests { 13 | // 14 | // @Test 15 | // public void contextLoads() { 16 | // } 17 | // 18 | //} 19 | --------------------------------------------------------------------------------