diff --git a/dayco-app/src/App.js b/dayco-app/src/App.js index 9fdf49a..1ace4d2 100644 --- a/dayco-app/src/App.js +++ b/dayco-app/src/App.js @@ -12,6 +12,7 @@ import { getCurrentUser } from './actions/user'; import { dispatchCreatePostsSuccess, dispatchCreatePostsFail, dispatchEditPostsSuccess, dispatchEditPostsFail, dispatchDeletePostsSuccess } from './actions/posts'; +import { dispatchPostsIncreaseLikeSuccess } from './actions/postsLike'; import { API_BASE_URL } from './constants'; import SockJsClient from 'react-stomp'; import Cookies from "js-cookie"; @@ -42,18 +43,19 @@ class App extends Component { this.clientRef = el} url= {API_BASE_URL + "/dayco-websocket"} - topics = {["/topic/posts"]} + topics = {["/topic/posts", "/topic/posts/like"]} headers= {customHeaders} subscribeHeaders={customHeaders} //message 보냈을 때의 callback onMessage={(msg) => { - console.log(msg) - if(this.props.status === 'create') { + if(this.props.socketActionStatus === 'postsCreate') { this.props.dispatchCreatePostsSuccess(msg); - } else if(this.props.status === 'edit') { + } else if(this.props.socketActionStatus === 'postsEdit') { this.props.dispatchEditPostsSuccess(msg); - } else { + } else if(this.props.socketActionStatus === 'postsDelete') { this.props.dispatchDeletePostsSuccess(msg.id, msg.author); + } else if(this.props.socketActionStatus == 'postsLikeIncrease') { + this.props.dispatchPostsIncreaseLikeSuccess(msg.id, msg.likeCount) } }} onConnectFailure={(error)=> console.log("Connect Fail : " + error)} @@ -67,7 +69,7 @@ class App extends Component { - + } /> @@ -80,9 +82,10 @@ class App extends Component { const mapStateToProps = (state) => { return { user: state.user, - status: state.postsEditModal.status, + socketActionStatus: state.socket.actionStatus, }; } export default connect(mapStateToProps, {getCurrentUser, - dispatchCreatePostsSuccess, dispatchEditPostsSuccess, dispatchDeletePostsSuccess + dispatchCreatePostsSuccess, dispatchEditPostsSuccess, dispatchDeletePostsSuccess, + dispatchPostsIncreaseLikeSuccess })(App); \ No newline at end of file diff --git a/dayco-app/src/actions/posts.js b/dayco-app/src/actions/posts.js index 0ed372a..7ee6132 100644 --- a/dayco-app/src/actions/posts.js +++ b/dayco-app/src/actions/posts.js @@ -254,4 +254,28 @@ export function hidePostsEditModal() { type: types.postsEditModal.MODAL_HIDE }) }; +} + +export function dispatchBeforeCreatePosts() { + return dispatch => { + dispatch({ + type: types.actionStatus.SOCKET_POST_CREATE + }) + }; +} + +export function dispatchBeforeEditPosts() { + return dispatch => { + dispatch({ + type: types.actionStatus.SOCKET_POST_EDIT + }) + }; +} + +export function dispatchBeforeDelPosts() { + return dispatch => { + dispatch({ + type: types.actionStatus.SOCKET_POST_DELETE + }) + }; } \ No newline at end of file diff --git a/dayco-app/src/actions/postsLike.js b/dayco-app/src/actions/postsLike.js new file mode 100644 index 0000000..213392e --- /dev/null +++ b/dayco-app/src/actions/postsLike.js @@ -0,0 +1,69 @@ +import * as types from '../constants/types'; +import * as API from '../services/http'; + +export function getPostsLikeCount(id) { + return dispatch => { + return API.getPostsLikeCount(id) + .then(async(response) => { + dispatch({ + type: types.postsLikeCount.GET_SUCCESS, + like: response.data + }) + }).catch(function (error) { + dispatch({ + type: types.postsLikeCount.GET_FAIL + }) + }) + } +} + +export function increaseLikeCount(id) { + return dispatch => { + return API.increaseLikeCount(id) + .then(async(response) => { + dispatch({ + type: types.postsLikeCount.INCREASE_SUCCESS, + id: response.data.id, + likeCount: response.data.likeCount + }); + }).catch(function (error) { + dispatch({ + type: types.postsLikeCount.INCREASE_FAIL + }) + }) + } +} + +export function dispatchPostsIncreaseLikeSuccess(id, likeCount) { + return dispatch => { + dispatch({ + type: types.postsLikeCount.INCREASE_SUCCESS, + id: id, + likeCount: likeCount + }) + } +} + +export function dispatchBeforeIncreaseLike() { + return dispatch => { + dispatch({ + type: types.actionStatus.SOCKET_POST_LIKE_INCREASE + }) + } +} + +export function dispatchBeforeDecreaseLike() { + return dispatch => { + dispatch({ + type: types.actionStatus.SOCKET_POST_LIKE_DECREASE + }) + } +} + +export function dispatchBeforeGetLike() { + return dispatch => { + dispatch({ + type: types.actionStatus.SOCKET_POST_LIKE_GET + }) + } +} \ No newline at end of file diff --git a/dayco-app/src/components/posts/PostEditModal.js b/dayco-app/src/components/posts/PostEditModal.js index abec675..bc485de 100644 --- a/dayco-app/src/components/posts/PostEditModal.js +++ b/dayco-app/src/components/posts/PostEditModal.js @@ -2,7 +2,9 @@ import React, { Component } from 'react'; import { Form, Button, Modal } from 'react-bootstrap'; import { connect } from 'react-redux'; import { withRouter } from "react-router"; -import { createPosts, editPosts, deletePosts, hidePostsEditModal } from '../../actions/posts'; +import { createPosts, editPosts, deletePosts, + dispatchBeforeCreatePosts, dispatchBeforeEditPosts, dispatchBeforeDelPosts, + hidePostsEditModal } from '../../actions/posts'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSmileBeam, faSms } from '@fortawesome/free-solid-svg-icons' import Cookies from "js-cookie"; @@ -17,7 +19,7 @@ class PostsEditModal extends Component { requestContent: '' } this.requestTitleChange = this.requestTitleChange.bind(this); - this.requestContentChange = this.requestContentChange.bind(this); + this.requestContentChange = this.requestContentChange.bind(this); } hideModal() { @@ -34,12 +36,13 @@ class PostsEditModal extends Component { createPosts = () => { if(this.props.isSocket === true) { + this.props.dispatchBeforeCreatePosts(); let jsonStr = JSON.stringify({ type: "create", title: this.state.requestTitle, content: this.state.requestContent }) - this.sendMessage("/app/dayco-websocket", jsonStr); + this.sendMessage("/app/posts", jsonStr); } else { this.props.createPosts(this.state.requestTitle, this.state.requestContent); } @@ -47,13 +50,14 @@ class PostsEditModal extends Component { editPosts = () => { if(this.props.isSocket === true) { + this.props.dispatchBeforeEditPosts(); let jsonStr = JSON.stringify({ type: "edit", postsId: this.props.id, title: this.state.requestTitle, content: this.state.requestContent }) - this.sendMessage("/app/dayco-websocket", jsonStr); + this.sendMessage("/app/posts", jsonStr); } else { this.props.editPosts(this.props.id, this.state.requestTitle, this.state.requestContent, this.props.author) } @@ -61,11 +65,12 @@ class PostsEditModal extends Component { deletePosts = () => { if(this.props.isSocket === true) { + this.props.dispatchBeforeDelPosts(); let jsonStr = JSON.stringify({ type: "delete", postsId: this.props.id }) - this.sendMessage("/app/dayco-websocket", jsonStr); + this.sendMessage("/app/posts", jsonStr); } else { this.props.deletePosts(this.props.id, this.props.author); } @@ -159,4 +164,5 @@ const mapStateToProps = (state) => { } export default withRouter(connect(mapStateToProps, {createPosts, editPosts, deletePosts, + dispatchBeforeCreatePosts, dispatchBeforeEditPosts, dispatchBeforeDelPosts, hidePostsEditModal})(PostsEditModal)); diff --git a/dayco-app/src/components/posts/Posts.js b/dayco-app/src/components/posts/Posts.js index 5400654..3773413 100644 --- a/dayco-app/src/components/posts/Posts.js +++ b/dayco-app/src/components/posts/Posts.js @@ -1,29 +1,67 @@ import React, { Component } from 'react'; import { Card, ListGroup, ListGroupItem, Badge, DropdownButton, DropdownItem } from 'react-bootstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCoffee, faRainbow, faRemoveFormat } from '@fortawesome/free-solid-svg-icons' -import { showPostsEditModal, showPostsDeleteModal, deletePosts } from '../../actions/posts'; +import { faCoffee, faThumbsUp, faThumbtack, faRainbow, faRemoveFormat, faComment } from '@fortawesome/free-solid-svg-icons' +import { showPostsEditModal, showPostsDeleteModal } from '../../actions/posts'; +import { getPostsLikeCount, increaseLikeCount, + dispatchBeforeIncreaseLike, dispatchBeforeDecreaseLike, dispatchBeforeGetLike } from '../../actions/postsLike'; import { connect } from 'react-redux'; import { withRouter } from "react-router"; +import Cookies from "js-cookie"; class Posts extends Component { - constructor(){ - super(...arguments); - this.handleSelect = this.handleSelect.bind(this); - } + constructor(){ + super(...arguments); + this.handleSelect = this.handleSelect.bind(this); + } + + handleSelect(e) { + if(e === 'edit') { + this.props.showPostsEditModal(this.props.id, this.props.title, this.props.content, this.props.author); + } else if(e === 'delete') { + this.props.showPostsDeleteModal(this.props.id, this.props.title, this.props.author); + } + } - handleSelect(e) { - if(e === 'edit') { - this.props.showPostsEditModal(this.props.id, this.props.title, this.props.content, this.props.author); - } else if(e === 'delete') { - this.props.showPostsDeleteModal(this.props.id, this.props.title, this.props.author); - } + increaseLikeCnt = (postId) => { + if(this.props.isSocket === true) { + this.props.dispatchBeforeIncreaseLike(); + let jsonStr = JSON.stringify({ + type: "increase", + postsId: postId + }) + this.sendMessage("/app/posts/like", jsonStr); + } else { + this.props.increaseLikeCount(postId); } + }; + + componentDidMount() { + this.props.getPostsLikeCount(this.props.id); + } + + sendMessage = (topic, jsonStr) => { + const token = Cookies.get("token") ? Cookies.get("token") : null; + const customHeaders = { + "Authorization": token + }; + this.props.clientRef.sendMessage(topic, + jsonStr, + customHeaders) + } render(){ const modifiedDateStr = new Date(this.props.modifiedDate); - return ( + + /** + * Like ID 가 일치할 경우, 일치하는 건수를 보여준다. + * 없을 경우, 0 으로 보여준다. + */ + const likes = this.props.postsEtc.likes; + const filteredLike = likes.filter(like => (like.id == this.props.id)); + const likeCount = (filteredLike.length > 0) ? filteredLike[0].likeCount : 0; + return ( } @@ -50,8 +88,14 @@ class Posts extends Component { - Like - Comment + + { this.increaseLikeCnt(this.props.id) }} />  + {likeCount} +    + + )}; @@ -59,9 +103,12 @@ class Posts extends Component { const mapStateToProps = (state) => { return { - posts: state.posts + posts: state.posts, + postsEtc: state.postsEtc, + isSocket: state.socket.isSocket }; } - -export default withRouter(connect(mapStateToProps, {showPostsEditModal, showPostsDeleteModal, deletePosts})(Posts)); +export default withRouter(connect(mapStateToProps, {showPostsEditModal, showPostsDeleteModal, + getPostsLikeCount, increaseLikeCount, + dispatchBeforeIncreaseLike, dispatchBeforeDecreaseLike, dispatchBeforeGetLike})(Posts)); diff --git a/dayco-app/src/components/posts/PostsList.js b/dayco-app/src/components/posts/PostsList.js index e5be43c..044710b 100644 --- a/dayco-app/src/components/posts/PostsList.js +++ b/dayco-app/src/components/posts/PostsList.js @@ -26,7 +26,8 @@ class PostsList extends Component { author={info.author} content={info.content} modifiedDate={info.modifiedDate} - />; + clientRef={this.props.clientRef} + />; }) return ( diff --git a/dayco-app/src/constants/initialState.js b/dayco-app/src/constants/initialState.js index b4d19f4..bebc319 100644 --- a/dayco-app/src/constants/initialState.js +++ b/dayco-app/src/constants/initialState.js @@ -16,6 +16,10 @@ export default { page: 0, rowNum : 0 }, + postsEtc: { //posts comment, like 처리 + likes: [], + comment: [] + }, postsEditModal: { isShow: false, id: -1, @@ -28,6 +32,7 @@ export default { list: [], }, socket: { - isSocket: true + isSocket: true, + actionStatus: "" } } diff --git a/dayco-app/src/constants/types.js b/dayco-app/src/constants/types.js index ca9912e..302fd1f 100644 --- a/dayco-app/src/constants/types.js +++ b/dayco-app/src/constants/types.js @@ -32,4 +32,22 @@ export const postsEditModal = { export const alert = { ALERT_CREATE : 'dayco-app/alert/create', ALERT_REMOVE : 'dayco-app/alert/remove' +} + +export const postsLikeCount = { + GET_SUCCESS: 'dayco-app/posts/like/get/success', + INCREASE_SUCCESS : 'dayco-app/posts/like/increase/success', + DECREASE_SUCCESS : 'dayco-app/posts/like/decrease/success', + GET_FAIL: 'dayco-app/posts/like/get/fail', + INCREASE_FAIL : 'dayco-app/posts/like/increase/fail', + DECREASE_FAIL : 'dayco-app/posts/like/decrease/fail' +} + +export const actionStatus = { + SOCKET_POST_CREATE : 'dayco-app/socket/posts/create', + SOCKET_POST_EDIT : 'dayco-app/socket/posts/edit', + SOCKET_POST_DELETE : 'dayco-app/socket/posts/delete', + SOCKET_POST_LIKE_GET :'dayco-app/socket/posts/like/get', + SOCKET_POST_LIKE_INCREASE :'dayco-app/socket/posts/like/increase', + SOCKET_POST_LIKE_DECREASE :'dayco-app/socket/posts/like/decrease' } \ No newline at end of file diff --git a/dayco-app/src/reducers/postsEtc.js b/dayco-app/src/reducers/postsEtc.js new file mode 100644 index 0000000..2c7c79e --- /dev/null +++ b/dayco-app/src/reducers/postsEtc.js @@ -0,0 +1,67 @@ +import initialState from '../constants/initialState'; +import * as types from '../constants/types'; + +/** + * Like, Comment 처리 + * @param {*} state + * @param {*} action + */ +export function postsEtc(state = initialState.postsEtc, action) { + switch (action.type) { + case types.postsLikeCount.GET_SUCCESS: + const { like } = action; + let duplicatedLike = state.likes.filter(l => (l.id == like.id)) + /** + * 중복되는 Like가 있을 경우, 하나로 통일 시킨다. + * 중복되는 Like가 없을 경우, 추가시킨다. + */ + const changedLikes = (duplicatedLike.length > 0) ? state.likes.map((l) => { + if(l.id === like.id) { + return Object.assign(like, { + id: like.id, + likeCount: likeCount + }); + } + return like; + }) : [ + like, ...state.likes + ] + return Object.assign({}, state, {likes: [...changedLikes]}); + case types.postsLikeCount.INCREASE_SUCCESS: + /** + * Like 건수 목록 중에 변경됬을 경우, + * 해당 항목만 변경시킨다. + */ + const { id, likeCount } = action; + const editLikesList = state.likes.map((like) => { + if(id === like.id) { + return Object.assign(like, { + id: like.id, + likeCount: likeCount + }); + } + return like; + }); + return Object.assign({}, state, { + likes: [...editLikesList] + }); + case types.postsLikeCount.DECREASE_SUCCESS: + return state; + case types.postsLikeCount.INCREASE_BEFORE: + return Object.assign({}, state, { + status: 'increment' + }); + case types.postsLikeCount.DECREASE_BEFORE: + return Object.assign({}, state, { + status: 'decrement' + }); + case types.postsLikeCount.GET_FAIL: + return state; + case types.postsLikeCount.INCREASE_FAIL: + return state; + case types.postsLikeCount.DECREASE_FAIL: + return state; + default: + return state; + } +} diff --git a/dayco-app/src/reducers/root.js b/dayco-app/src/reducers/root.js index 26a87fd..d96024c 100644 --- a/dayco-app/src/reducers/root.js +++ b/dayco-app/src/reducers/root.js @@ -2,6 +2,7 @@ import { combineReducers } from 'redux'; import { user } from './user'; import { posts, postsEditModal } from './posts'; +import { postsEtc } from './postsEtc'; import { alerts } from './alerts'; import { socket } from './socket'; @@ -12,6 +13,7 @@ import { socket } from './socket'; const rootReducer = combineReducers({ user, posts, + postsEtc, postsEditModal, alerts, socket diff --git a/dayco-app/src/reducers/socket.js b/dayco-app/src/reducers/socket.js index e8c4cfe..f327cb9 100644 --- a/dayco-app/src/reducers/socket.js +++ b/dayco-app/src/reducers/socket.js @@ -3,12 +3,38 @@ import * as types from '../constants/types'; /** - * The posts reducer is responsible + * The socket reducer is responsible + * Web Socket 사용 시, 한 곳에서 메시지를 처리하기 때문에 상태 별로 구분지어서 상태 분리 + * * @param {*} state * @param {*} action */ export function socket(state = initialState.socket, action) { switch (action.type) { + case types.actionStatus.SOCKET_POST_CREATE: + return Object.assign({}, state, { + actionStatus: 'postsCreate' + }); + case types.actionStatus.SOCKET_POST_EDIT: + return Object.assign({}, state, { + actionStatus: 'postsEdit' + }); + case types.actionStatus.SOCKET_POST_DELETE: + return Object.assign({}, state, { + actionStatus: 'postsDelete' + }); + case types.actionStatus.SOCKET_POST_LIKE_GET: + return Object.assign({}, state, { + actionStatus: 'postsLikeGet' + }); + case types.actionStatus.SOCKET_POST_LIKE_INCREASE: + return Object.assign({}, state, { + actionStatus: 'postsLikeIncrease' + }); + case types.actionStatus.SOCKET_POST_LIKE_DECREASE: + return Object.assign({}, state, { + actionStatus: 'postsLikeDecrease' + }); default: return state; } diff --git a/dayco-app/src/services/http.js b/dayco-app/src/services/http.js index dc6ba17..9d8087f 100644 --- a/dayco-app/src/services/http.js +++ b/dayco-app/src/services/http.js @@ -92,4 +92,25 @@ export function getAllPosts() { 'Authorization': token } }) +} + +export function getPostsLikeCount(postsId) { + const token = Cookies.get("token") ? Cookies.get("token") : null; + return axios.get(API_BASE_URL + "/posts/likes/" + postsId, { + headers: { + 'Content-type': 'application/json', + 'Authorization': token + } + }); +} + +export function increaseLikeCount(postsId) { + const token = Cookies.get("token") ? Cookies.get("token") : null; + return axios.post(API_BASE_URL + "/posts/like/increase/" + postsId, null, + { + headers: { + 'Content-type': 'application/json', + 'Authorization': token + } + }); } \ No newline at end of file diff --git a/dayco-gateway/build.gradle b/dayco-gateway/build.gradle index 7e84ff9..7dc1a1a 100644 --- a/dayco-gateway/build.gradle +++ b/dayco-gateway/build.gradle @@ -16,7 +16,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '2.2.5.RELEASE' compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-gateway', version: '2.2.5.RELEASE' - compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-eureka-client', version: '2.2.1.RELEASE' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } diff --git a/dayco-gateway/src/main/java/io/github/dayco/gateway/config/RibbonConfiguration.java b/dayco-gateway/src/main/java/io/github/dayco/gateway/config/RibbonConfiguration.java deleted file mode 100644 index ef6a001..0000000 --- a/dayco-gateway/src/main/java/io/github/dayco/gateway/config/RibbonConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.github.dayco.gateway.config; - -import org.springframework.context.annotation.Bean; - -import com.netflix.client.config.IClientConfig; -import com.netflix.loadbalancer.AvailabilityFilteringRule; -import com.netflix.loadbalancer.IPing; -import com.netflix.loadbalancer.IRule; -import com.netflix.loadbalancer.PingUrl; - -public class RibbonConfiguration { - - /** - * Returns the {@link IPing} changes the default state checking mechanism - * - * @param config the {@link IClientConfig} - */ - @Bean - public IPing ribbonPing(final IClientConfig config) { - return new PingUrl(false, "/actuator/health"); - } - - /** - * Returns the {@link IRule} to modify default load balancing strategy - * - * @param config the {@link IClientConfig} - */ - @Bean - public IRule ribbonRule(final IClientConfig config) { - return new AvailabilityFilteringRule(); - } -} diff --git a/dayco/build.gradle b/dayco/build.gradle index 22111c1..d9a984f 100644 --- a/dayco/build.gradle +++ b/dayco/build.gradle @@ -22,6 +22,7 @@ dependencies { compile group: 'org.springframework.boot', name: 'spring-boot-starter-hateoas', version: '1.2.2.RELEASE' compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '2.2.4.RELEASE' compile group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '2.3.3.RELEASE' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.3.4.RELEASE' compile group: 'org.springframework.plugin', name: 'spring-plugin-core', version: '2.0.0.RELEASE' implementation 'org.springframework.boot:spring-boot-starter' diff --git a/dayco/src/main/java/io/github/dayco/common/config/RedisRepositoryConfig.java b/dayco/src/main/java/io/github/dayco/common/config/RedisRepositoryConfig.java new file mode 100644 index 0000000..21d2cee --- /dev/null +++ b/dayco/src/main/java/io/github/dayco/common/config/RedisRepositoryConfig.java @@ -0,0 +1,36 @@ +package io.github.dayco.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisRepositoryConfig { + + @Value("${spring.redis.host}") + private String redisHost; + + @Value("${spring.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } + +} diff --git a/dayco/src/main/java/io/github/dayco/external/config/UaaApiRequestConfiguration.java b/dayco/src/main/java/io/github/dayco/external/config/UaaApiRequestConfiguration.java deleted file mode 100644 index 07644a3..0000000 --- a/dayco/src/main/java/io/github/dayco/external/config/UaaApiRequestConfiguration.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.dayco.external.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; - -import feign.RequestInterceptor; - -public class UaaApiRequestConfiguration { - -// @Bean -// public RequestInterceptor uaaApiRequestHeader( -// @Value("${feign.user-api.bearerToken}") String token -// ) { -// return new BearerAuthRequestInterceptor(token); -// } -} diff --git a/dayco/src/main/java/io/github/dayco/posts/application/PostsService.java b/dayco/src/main/java/io/github/dayco/posts/application/PostsService.java index f349c5c..8822d6c 100644 --- a/dayco/src/main/java/io/github/dayco/posts/application/PostsService.java +++ b/dayco/src/main/java/io/github/dayco/posts/application/PostsService.java @@ -8,6 +8,8 @@ import io.github.dayco.posts.domain.Posts; import io.github.dayco.posts.infra.PostsJpaRepository; +import io.github.dayco.posts.infra.PostsRedisRepository; +import io.github.dayco.posts.ui.dto.PostsLikeDto; import io.github.dayco.posts.ui.dto.PostsListResponseDto; import io.github.dayco.posts.ui.dto.PostsResponseDto; import io.github.dayco.posts.ui.dto.PostsSaveRequestDto; @@ -21,6 +23,8 @@ public class PostsService { private final PostsJpaRepository postsJpaRepository; + private final PostsRedisRepository postsRedisRepository; + @Transactional public Posts save(PostsSaveRequestDto requestDto) { return postsJpaRepository.save( @@ -58,4 +62,18 @@ public List findAll() { .map(PostsListResponseDto::new) .collect(Collectors.toList()); } + + public PostsLikeDto increaseLike(Long id, String userId) { + return new PostsLikeDto(id, userId, + postsRedisRepository.increaseLikeCount(id, userId)); + } + + public PostsLikeDto decreaseLike(Long id, String userId) { + return new PostsLikeDto(id, userId, + postsRedisRepository.decreaseLikeCount(id, userId)); + } + + public PostsLikeDto getLikes(Long id) { + return new PostsLikeDto(id, postsRedisRepository.getLikeCount(id)); + } } diff --git a/dayco/src/main/java/io/github/dayco/posts/infra/PostsRedisRepository.java b/dayco/src/main/java/io/github/dayco/posts/infra/PostsRedisRepository.java new file mode 100644 index 0000000..b6bca35 --- /dev/null +++ b/dayco/src/main/java/io/github/dayco/posts/infra/PostsRedisRepository.java @@ -0,0 +1,37 @@ +package io.github.dayco.posts.infra; + +import java.util.Optional; + +import javax.annotation.Resource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +@Component +public class PostsRedisRepository { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Resource(name = "redisTemplate") + private ValueOperations valueOps; + + + public static final String POSTS_LIKE = "POSTS_LIKE"; + + public long increaseLikeCount(Long postsId, String userId) { + return Optional.ofNullable(valueOps.increment(POSTS_LIKE + "_" + postsId)) + .orElse(0L); + } + + public long decreaseLikeCount(Long postsId, String userId) { + return Optional.ofNullable(valueOps.decrement(POSTS_LIKE + "_" + postsId)) + .filter(count -> count > 0).orElse(0L); + } + + public long getLikeCount(Long postsId) { + return Long.valueOf(Optional.ofNullable(valueOps.get(POSTS_LIKE + "_" + postsId)) + .orElse("0")); + } +} diff --git a/dayco/src/main/java/io/github/dayco/posts/ui/PostsController.java b/dayco/src/main/java/io/github/dayco/posts/ui/PostsController.java index b7ad255..78ff977 100644 --- a/dayco/src/main/java/io/github/dayco/posts/ui/PostsController.java +++ b/dayco/src/main/java/io/github/dayco/posts/ui/PostsController.java @@ -24,6 +24,7 @@ import io.github.dayco.external.ui.vo.User; import io.github.dayco.posts.application.PostsService; import io.github.dayco.posts.domain.Posts; +import io.github.dayco.posts.ui.dto.PostsLikeDto; import io.github.dayco.posts.ui.dto.PostsListResponseDto; import io.github.dayco.posts.ui.dto.PostsResponseDto; import io.github.dayco.posts.ui.dto.PostsSaveRequestDto; @@ -90,4 +91,42 @@ public Result delete(@PathVariable Long id) { public List all() { return postsService.findAll(); } + + + /** + * Posts Like 건수를 증가시킨다. + * @param id Posts ID + * @param authorizationHeader + * @return Like 건수 + */ + @PostMapping("/like/increase/{id}") + public PostsLikeDto increaseLike(@PathVariable Long id, + @RequestHeader(value = "Authorization") String authorizationHeader) { + User user = userClient.getCurrentUser(authorizationHeader); + if(user == null) { + throw new IllegalArgumentException("User doesn't Exist"); + } + return postsService.increaseLike(id, user.getUserId()); + } + + /** + * Posts Like 건수를 감소시킨다. + * @param id Posts ID + * @param authorizationHeader + * @return Like 건수 + */ + @PostMapping("/like/decrease/{id}") + public PostsLikeDto decreaseLike(@PathVariable Long id, + @RequestHeader(value = "Authorization") String authorizationHeader) { + User user = userClient.getCurrentUser(authorizationHeader); + if(user == null) { + throw new IllegalArgumentException("User doesn't Exist"); + } + return postsService.decreaseLike(id, user.getUserId()); + } + + @GetMapping("/likes/{id}") + public PostsLikeDto getLikes(@PathVariable Long id) { + return postsService.getLikes(id); + } } diff --git a/dayco/src/main/java/io/github/dayco/posts/ui/PostsSocketController.java b/dayco/src/main/java/io/github/dayco/posts/ui/PostsSocketController.java index f3be6c1..3c2dd36 100644 --- a/dayco/src/main/java/io/github/dayco/posts/ui/PostsSocketController.java +++ b/dayco/src/main/java/io/github/dayco/posts/ui/PostsSocketController.java @@ -15,6 +15,8 @@ import io.github.dayco.external.ui.vo.User; import io.github.dayco.posts.application.PostsService; import io.github.dayco.posts.domain.Posts; +import io.github.dayco.posts.ui.dto.PostsLikeDto; +import io.github.dayco.posts.ui.dto.PostsLikeMessage; import io.github.dayco.posts.ui.dto.PostsMessage; import io.github.dayco.posts.ui.dto.PostsSaveRequestDto; import io.github.dayco.posts.ui.dto.PostsUpdateRequestDto; @@ -36,8 +38,7 @@ public PostsSocketController(PostsService postsService, this.messageSourceAccessor = messageSourceAccessor; } - - @MessageMapping("/dayco-websocket") + @MessageMapping("/posts") @SendTo("/topic/posts") public Posts message(PostsMessage postsMessage, @Header("Authorization") String authorizationHeader) { @@ -74,4 +75,35 @@ public Posts message(PostsMessage postsMessage, } return posts; } + + + @MessageMapping("/posts/like") + @SendTo("/topic/posts/like") + public PostsLikeDto increaseLikeMessage(PostsLikeMessage postsLikeMessage, + @Header("Authorization") String authorizationHeader) { + User user = userClient.getCurrentUser(authorizationHeader); + if(user == null) { + throw new IllegalArgumentException("User doesn't Exist"); + } + + if(postsLikeMessage == null) { + throw new IllegalArgumentException("PostsLikeMessage doesn't Exist"); + } + System.out.println("increaseLikeMessage Posts ID:: " + postsLikeMessage.getPostsId()); + PostsLikeDto postsLikeDto = null; + switch (postsLikeMessage.getType()) { + case "increase": + postsLikeDto = postsService.increaseLike(postsLikeMessage.getPostsId(), + user.getUserId()); + break; + case "decrease": + postsLikeDto = postsService.decreaseLike(postsLikeMessage.getPostsId(), + user.getUserId()); + break; + default: + postsLikeDto = postsService.getLikes(postsLikeMessage.getPostsId()); + break; + } + return postsLikeDto; + } } diff --git a/dayco/src/main/java/io/github/dayco/posts/ui/dto/PostsLikeDto.java b/dayco/src/main/java/io/github/dayco/posts/ui/dto/PostsLikeDto.java new file mode 100644 index 0000000..20af55c --- /dev/null +++ b/dayco/src/main/java/io/github/dayco/posts/ui/dto/PostsLikeDto.java @@ -0,0 +1,24 @@ +package io.github.dayco.posts.ui.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PostsLikeDto { + + private Long id; + private String userId; + private Long likeCount; + + public PostsLikeDto(Long id, Long likeCount) { + this.id = id; + this.likeCount = likeCount; + } + + public PostsLikeDto(Long id, String userId, Long likeCount) { + this.id = id; + this.userId = userId; + this.likeCount = likeCount; + } +} diff --git a/dayco/src/main/java/io/github/dayco/posts/ui/dto/PostsLikeMessage.java b/dayco/src/main/java/io/github/dayco/posts/ui/dto/PostsLikeMessage.java new file mode 100644 index 0000000..cb79b68 --- /dev/null +++ b/dayco/src/main/java/io/github/dayco/posts/ui/dto/PostsLikeMessage.java @@ -0,0 +1,27 @@ +package io.github.dayco.posts.ui.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@ToString +@Getter +@Setter +public class PostsLikeMessage { + + public PostsLikeMessage() {} + + @Builder + public PostsLikeMessage(String type, Long postsId, + String sender) { + this.type = type; + this.postsId = postsId; + this.sender = sender; + } + + private String type; // 타입 (increase, decrease ) + private Long postsId; // 게시판 번호 + private String sender; + +} diff --git a/dayco/src/main/java/io/github/dayco/socket/config/EmbeddedRedisConfig.java b/dayco/src/main/java/io/github/dayco/socket/config/EmbeddedRedisConfig.java index 3f0b196..8111aec 100644 --- a/dayco/src/main/java/io/github/dayco/socket/config/EmbeddedRedisConfig.java +++ b/dayco/src/main/java/io/github/dayco/socket/config/EmbeddedRedisConfig.java @@ -1,6 +1,8 @@ package io.github.dayco.socket.config; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -8,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.util.StringUtils; import redis.embedded.RedisServer; @@ -25,7 +28,8 @@ public class EmbeddedRedisConfig { @PostConstruct public void redisServer() throws IOException { - redisServer = new RedisServer(redisPort); + int port = isRedisRunning()? findAvailablePort() : redisPort; + redisServer = new RedisServer(port); redisServer.start(); } @@ -35,4 +39,50 @@ public void stopRedis() { redisServer.stop(); } } + + /** + * Embedded Redis 가 현재 실행중인지 확인 + */ + private boolean isRedisRunning() throws IOException { + return isRunning(executeGrepProcessCommand(redisPort)); + } + + /** + * 현재 PC/서버에서 사용가능한 포트 조회 + */ + public int findAvailablePort() throws IOException { + for (int port = 10000; port <= 65535; port++) { + Process process = executeGrepProcessCommand(port); + if (!isRunning(process)) { + return port; + } + } + throw new IllegalArgumentException("Not Found Available port: 10000 ~ 65535"); + } + + /** + * 해당 port 를 사용중인 프로세스 확인하는 sh 실행 + */ + private Process executeGrepProcessCommand(int port) throws IOException { + String command = String.format("netstat -nat | grep LISTEN|grep %d", port); + String[] shell = {"/bin/sh", "-c", command}; + return Runtime.getRuntime().exec(shell); + } + + /** + * 해당 Process 가 현재 실행중인지 확인 + */ + private boolean isRunning(Process process) { + String line; + StringBuilder pidInfo = new StringBuilder(); + + try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + while ((line = input.readLine()) != null) { + pidInfo.append(line); + } + } catch (Exception e) { + } + return !StringUtils.isEmpty(pidInfo.toString()); + } + } diff --git a/dayco/src/main/resources/application-dev.yml b/dayco/src/main/resources/application-dev.yml new file mode 100644 index 0000000..954adb9 --- /dev/null +++ b/dayco/src/main/resources/application-dev.yml @@ -0,0 +1,6 @@ +spring: + profiles: + active: local + redis: + host: localhost + port: 6379 \ No newline at end of file diff --git a/dayco/src/main/resources/application-real.yml b/dayco/src/main/resources/application-real.yml new file mode 100644 index 0000000..cb0662a --- /dev/null +++ b/dayco/src/main/resources/application-real.yml @@ -0,0 +1,6 @@ +spring: + profiles: + active: + redis: + host: + port: 6379 \ No newline at end of file diff --git a/dayco/src/main/resources/application.yml b/dayco/src/main/resources/application.yml index 1d04599..5229ddf 100644 --- a/dayco/src/main/resources/application.yml +++ b/dayco/src/main/resources/application.yml @@ -5,6 +5,8 @@ spring: application: name: ${rootProject.name} version: ${version} + profiles: + active: dev #dev, real h2: console: enabled: true diff --git a/dayco/src/test/java/io/github/dayco/posts/infra/PostsRedisRepositoryTest.java b/dayco/src/test/java/io/github/dayco/posts/infra/PostsRedisRepositoryTest.java new file mode 100644 index 0000000..869d4b1 --- /dev/null +++ b/dayco/src/test/java/io/github/dayco/posts/infra/PostsRedisRepositoryTest.java @@ -0,0 +1,24 @@ +package io.github.dayco.posts.infra; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class PostsRedisRepositoryTest { + + @Autowired + private PostsRedisRepository postsRedisRepository; + + @Test + public void increaseLikeCount_True() { + postsRedisRepository.increaseLikeCount(1L, "test"); + + Long likeCount = postsRedisRepository.getLikeCount(1L); + assertEquals(likeCount, 1L); + } + +} diff --git a/dayco/src/test/resources/application.yml b/dayco/src/test/resources/application.yml new file mode 100644 index 0000000..cf41590 --- /dev/null +++ b/dayco/src/test/resources/application.yml @@ -0,0 +1,10 @@ +spring: + redis: + host: localhost + port: 6379 + profiles: + active: dev + + +feign: + user-api.url: http://localhost:9999 \ No newline at end of file