userDetail.jsp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
  2. <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
  3. <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
  4. <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
  5. <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
  6. <%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
  7. <jsp:include page="/WEB-INF/jsp/include/head.jsp"></jsp:include>
  8. <script>
  9. let targetFeed = null;
  10. let params = {
  11. articleId: -1,
  12. feed: 'your-feed'
  13. };
  14. let query = Object.keys(params)
  15. .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
  16. .join('&');
  17. let url = '/article/page/users/${user.id}?' + query;
  18. let loadingArticles = null;
  19. let articleList = null;
  20. const tagSet = new Set();
  21. const options = { method: 'GET' };
  22. const follow = 'Follow ' + "${user.username}";
  23. const unfollow = 'Unfollow ' + "${user.username}";
  24. // feed를 바꾼 경우
  25. const filterArticle = (option) => {
  26. const { clickedFeed } = option;
  27. const articles = document.querySelectorAll('.article-preview');
  28. articles.forEach(article => {
  29. let display = 'none';
  30. // display 기본 값은 none이고 값이 block으로 변경되는 경우만 조건으로 건다.
  31. if(clickedFeed === 'favorite-feed') {
  32. display = 'block';
  33. } else if(clickedFeed === 'your-feed') {
  34. display = 'block';
  35. }
  36. });
  37. }
  38. // 게시글 목록 로딩
  39. const setLoading = (type) => {
  40. let loadingDisplay = null;
  41. let contentDisplay = null;
  42. if(type === 'on') {
  43. loadingDisplay = 'block';
  44. contentDisplay = 'none';
  45. } else if(type === 'off') {
  46. loadingDisplay = 'none';
  47. contentDisplay = 'block';
  48. }
  49. loadingArticles.style.display = loadingDisplay;
  50. articleList.style.display = contentDisplay;
  51. }
  52. // 자신의 게시글이 없는 경우
  53. const noContent = () => {
  54. if(articleList.firstChild === null) {
  55. const p = document.createElement('p');
  56. p.classList.add('article-preview');
  57. p.textContent = 'No articles are here... yet.';
  58. articleList.appendChild(p);
  59. }
  60. }
  61. const focusFeed = () => {
  62. // 이미 포커싱되었으면 중단
  63. if(event.target.parentNode.classList.contains('active')) {
  64. return ;
  65. }
  66. const currentFeed = event.target.parentNode; // 포커싱할 피드
  67. const focusedFeed = (currentFeed.id === 'favorite-feed') ? document.querySelector('#your-feed') : document.querySelector('#favorite-feed'); // 현재 포커싱되어있는 피드
  68. currentFeed.classList.add('active');
  69. focusedFeed.classList.remove('active');
  70. // targetFeed 초기화
  71. targetFeed = currentFeed;
  72. setLoading('on');
  73. // 게시글 초기화
  74. while(articleList.firstChild) {
  75. articleList.firstChild.remove();
  76. }
  77. params = {
  78. articleId: -1,
  79. feed: targetFeed.id
  80. }
  81. query = Object.keys(params)
  82. .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
  83. .join('&');
  84. url = '/article/page/users/${user.id}?' + query;
  85. fetch(url, options)
  86. .then(response => response.json())
  87. .then(json => {
  88. const { articles, paging } = json;
  89. username = "${ssUsername}";
  90. displayArticles(articles);
  91. nextPageLoad(articles, paging);
  92. })
  93. filterArticle({ 'clickedFeed': currentFeed.id });
  94. setTimeout(() => {
  95. setLoading('off');
  96. noContent();
  97. }, 1000);
  98. }
  99. // 좋아요 버튼
  100. const favoriteBtn = (indexNumber) => {
  101. const favoriteNum = event.target.querySelector('.count').textContent;
  102. const favorite = event.target.querySelector('.favorite-btn');
  103. let counts = parseInt(favoriteNum);
  104. let newCounts = counts + 1;
  105. // 로그인한 경우에만 실행
  106. if("${ssIsLogin}" === "true") {
  107. if(event.target.classList.contains('active')) { // 좋아요 버튼 해지
  108. event.target.querySelector('.count').textContent = counts - 1;
  109. event.target.classList.remove('active');
  110. fetch('/article/' + indexNumber + '/favorite', { method: 'DELETE' })
  111. .then(response => {
  112. if(response.status === 200) {
  113. return ;
  114. }
  115. });
  116. } else { // 좋아요 버튼 활성
  117. event.target.querySelector('.count').textContent = newCounts;
  118. event.target.classList.add('active');
  119. fetch('/article/' + indexNumber + '/favorite', {
  120. body: JSON.stringify({
  121. articleId: "${article.id}",
  122. userId: "${ssId}",
  123. created: new Date()
  124. }),
  125. method: 'POST',
  126. headers: {
  127. "Content-Type": "application/json"
  128. }
  129. })
  130. .then(response => {
  131. if(response.status === 201) {
  132. return;
  133. }
  134. })
  135. }
  136. } else {
  137. location.href = "/user/signin";
  138. }
  139. }
  140. // 사용자 팔로우
  141. const userFollow = () => {
  142. const followBtn = document.querySelector('.follow-btn');
  143. if("${ssIsLogin}" === "true") { // 로그인한 경우에만 팔로우 기능 사용 가능
  144. if(followBtn.classList.contains('active')) { // 팔로우 취소
  145. followBtn.querySelector('.follow').textContent = follow;
  146. fetch("/user/unfollow", {
  147. body: JSON.stringify({
  148. fromUser: "${ssId}",
  149. toUser: "${user.id}"
  150. }),
  151. method: 'DELETE',
  152. headers: {
  153. "Content-Type": "application/json"
  154. }
  155. })
  156. .then(response => {
  157. if(response.status === 200) {
  158. return followBtn.classList.remove('active');
  159. }
  160. })
  161. } else { // 팔로우하기
  162. followBtn.querySelector('.follow').textContent = unfollow;
  163. fetch("/user/follow", {
  164. body: JSON.stringify({
  165. fromUser: "${ssId}",
  166. toUser: "${user.id}"
  167. }),
  168. method: 'POST',
  169. headers: {
  170. "Content-Type": 'application/json'
  171. }
  172. })
  173. .then(response => {
  174. if(response.status === 201) {
  175. return followBtn.classList.add('active');
  176. }
  177. })
  178. }
  179. } else { // 로그인하지 않은 경우 로그인화면으로 이동
  180. location.href = "/user/signin"
  181. }
  182. }
  183. // 게시글 목록 조회
  184. const displayArticles = (articles) => {
  185. articles.forEach(article => {
  186. console.log(JSON.stringify(article))
  187. const domParser = new DOMParser();
  188. // 게시글 정보
  189. const domStrArticleMeta =
  190. `
  191. <div class="article-meta">
  192. <div class="metadata">
  193. <a href="/user/\${article.writerId}">
  194. <img src="/resources/images/avatar.png" alt="avatar">
  195. </a>
  196. <div class="article-info">
  197. <a href="/user/\${article.writerId}" class="name">\${article.writerName}</a>
  198. <span class="date"></span>
  199. </div>
  200. </div>
  201. <div>
  202. <button class="favorite-btn" onclick="favoriteBtn(\${article.id})">
  203. <i class="fas fa-heart"></i>
  204. <span class="count">\${article.favoriteNum}</span>
  205. </button>
  206. </div>
  207. </div>
  208. `;
  209. const divArticleMeta = domParser.parseFromString(domStrArticleMeta, 'text/html').body.firstChild;
  210. divArticleMeta.querySelector('.date').textContent = new Date(article.created).toLocaleString();
  211. // 게시글 좋아요한 경우 active
  212. if (article.favorite) {
  213. divArticleMeta.querySelector('.favorite-btn').classList.add('active');
  214. }
  215. // 게시글 내용 및 게시글태그
  216. const domStrPreviewLink =
  217. `
  218. <a href="/article/\${article.id}" class="preview-link">
  219. <h1 class="preview-title">\${article.title}</h1>
  220. <p>\${article.subtitle}</p>
  221. <div class="tag-data">
  222. <span>Read more...</span>
  223. <ul class="tag-list">
  224. </ul>
  225. </div>
  226. </a>
  227. `;
  228. const aPreviewLink = domParser.parseFromString(domStrPreviewLink, 'text/html').body.firstChild;
  229. const ul = aPreviewLink.querySelector('.tag-list');
  230. if (article.tags !== '') {
  231. article.tags.split(',').forEach(tag => {
  232. if(!tagSet.has(tag)) {
  233. tagSet.add(tag);
  234. }
  235. const li = document.createElement('li');
  236. li.classList.add('tag');
  237. li.textContent = tag;
  238. ul.appendChild(li);
  239. });
  240. }
  241. // 게시글 완성
  242. const articlePreview = document.createElement('article');
  243. articlePreview.classList.add('article-preview');
  244. articlePreview.append(divArticleMeta, aPreviewLink);
  245. articlePreview.style.display = 'block';
  246. articleList.appendChild(articlePreview);
  247. })
  248. }
  249. const nextPageLoad = (articles, paging) => {
  250. // 더 조회될 수 있는 게시글이 있는지 여부
  251. if(paging.isNext) {
  252. const moreButton = document.createElement('button');
  253. const articleList = document.querySelector('#article-list');
  254. const loading = document.createElement('div');
  255. moreButton.textContent = '더보기';
  256. moreButton.classList.add('more-button');
  257. loading.setAttribute('id', 'loading');
  258. articleList.appendChild(moreButton);
  259. // more 버튼 클릭 시 다음페이지 게시물을 가져온다
  260. moreButton.onclick = () => {
  261. articleList.removeChild(moreButton);
  262. articleList.appendChild(loading); // 로딩 생성
  263. // 현재 페이지의 마지막 게시글 id
  264. params['articleId'] = articles[articles.length - 1].id;
  265. query = Object.keys(params)
  266. .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
  267. .join('&');
  268. url = '/article/page/users/${user.id}?' + query;
  269. fetch(url, options)
  270. .then(response => response.json())
  271. .then(json => {
  272. const {
  273. articles,
  274. paging
  275. } = json;
  276. username = "${ssUsername}";
  277. setTimeout(() => {
  278. articleList.removeChild(loading);
  279. displayArticles(articles);
  280. nextPageLoad(articles, paging);
  281. }, 1000);
  282. })
  283. }
  284. }
  285. }
  286. window.onload = () => {
  287. targetFeed = document.querySelector('.nav-item.active');
  288. loadingArticles = window.document.querySelector('#article-loading');
  289. articleList = document.querySelector('#article-list');
  290. const followString = document.querySelector('.follow-btn');
  291. if("${isFollow}" === "true"){
  292. followString.classList.add('active');
  293. followString.querySelector('.follow').textContent = unfollow;
  294. }
  295. setLoading('on');
  296. // fetch api call
  297. fetch(url, options)
  298. .then(response => response.json())
  299. .then(json => {
  300. const { articles, paging } = json;
  301. username = "${ssUsername}";
  302. displayArticles(articles);
  303. nextPageLoad(articles, paging);
  304. // hide loadings
  305. setTimeout(() => {
  306. setLoading('off');
  307. noContent();
  308. }, 1000);
  309. });
  310. }
  311. </script>
  312. </head>
  313. <body>
  314. <jsp:include page="/WEB-INF/jsp/include/header.jsp"></jsp:include>
  315. <!-- user-page content -->
  316. <div class="user-page">
  317. <!-- User Info -->
  318. <section class="user-info">
  319. <div class="container">
  320. <div class="row">
  321. <div class="col-10">
  322. <img src="/resources/images/avatar.png" alt="" class="user-img">
  323. <h4 class="user-name"><c:out value="${user.username}"></c:out></h4>
  324. <p class="profile-bio">
  325. <pre>
  326. <c:out value="${user.shortBio}"></c:out>
  327. </pre>
  328. </p>
  329. <c:choose>
  330. <c:when test="${user.id eq ssId}">
  331. <a href="/user/settings/${user.id}" class="action-btn btn-sm">
  332. <i class="fas fa-cog"></i>&nbsp;Edit Profile Settings
  333. </a>
  334. </c:when>
  335. <c:otherwise>
  336. <button class="action-btn btn-sm follow-btn" onclick="userFollow()">
  337. <i class="fas fa-plus"></i>
  338. <span class="follow">&nbsp;Follow <c:out value="${user.username}"/></span>
  339. </button>
  340. </c:otherwise>
  341. </c:choose>
  342. </div>
  343. </div>
  344. </div>
  345. </section>
  346. <!-- Body -->
  347. <div class="container main">
  348. <div class="row">
  349. <div class="col-10">
  350. <!-- 토글 버튼으로 피드 내용 보기 -->
  351. <div class="toggle">
  352. <ul class="nav">
  353. <li id="your-feed" class="nav-item active">
  354. <a href="javascript:void(0);" onclick="focusFeed()">My Articles</a>
  355. </li>
  356. <li id="favorite-feed" class="nav-item">
  357. <a href="javascript:void(0);" onclick="focusFeed()">Favorited Articles</a>
  358. </li>
  359. </ul>
  360. </div>
  361. <!-- Content -->
  362. <div class="article">
  363. <!-- Article Loading -->
  364. <article id="article-loading">
  365. Loading articles...
  366. </article>
  367. <!-- Article List -->
  368. <div id="article-list" style="display: block;"></div>
  369. </div>
  370. </div>
  371. </div>
  372. </div>
  373. </div>
  374. </body>
  375. </html>