main.jsp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  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 loadingTag = null;
  10. let loadingArticle = null;
  11. let articleList = null;
  12. let ssIsLogin = null;
  13. let targetFeed = null;
  14. const options = {
  15. method: 'GET'
  16. };
  17. const tagSet = new Set();
  18. let tagArray = [];
  19. // fetch url info
  20. const selectFeed = ("${ssIsLogin}" === "true") ? 'your-feed' : 'flobal-feed';
  21. let params = {
  22. articleId: -1,
  23. feed: selectFeed
  24. };
  25. let query = Object.keys(params)
  26. .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
  27. .join('&');
  28. let url = '/article/page?' + query;
  29. // 1. feed를 바꾼 경우
  30. // 2. 태그를 클릭한 경우
  31. const filterArticle = (option) => {
  32. const {
  33. clickedTag,
  34. clickedFeed
  35. } = option;
  36. const articles = window.document.querySelectorAll('.article-preview');
  37. const articleList = document.querySelector('#article-list');
  38. articles.forEach(article => {
  39. const writerName = article.querySelector('.name').textContent;
  40. let display = 'none';
  41. // display 기본 값은 none이고 값이 block으로 변경되는 경우만 조건으로 건다
  42. if (clickedFeed === 'global-feed') {
  43. display = 'block';
  44. } else if (clickedFeed === 'your-feed' && "${ssUsername}" === writerName) {
  45. display = 'block';
  46. } else if (clickedTag !== undefined) { // 태그가 클릭된 경우
  47. const tags = article.querySelectorAll('.tag');
  48. const tagSet = new Set(Array.from(tags).map(tag => tag.textContent));
  49. if (tagSet.has(clickedTag)) {
  50. display = 'block';
  51. }
  52. }
  53. article.style.display = display;
  54. });
  55. };
  56. const setLoading = (type) => {
  57. let loadingDisplay = null;
  58. let contentDisplay = null;
  59. if (type === 'on') {
  60. loadingDisplay = 'block';
  61. contentDisplay = 'none';
  62. } else if (type === 'off') {
  63. loadingDisplay = 'none';
  64. contentDisplay = 'block';
  65. }
  66. loadingTag.style.display = loadingDisplay;
  67. loadingArticle.style.display = loadingDisplay;
  68. articleList.style.display = contentDisplay;
  69. tagList.style.display = contentDisplay;
  70. }
  71. // 자신의 게시글이 없는 경우
  72. const noContent = () => {
  73. articleList = document.querySelector('#article-list');
  74. tagList = document.querySelector('#tag-list');
  75. if (articleList.firstChild === null) {
  76. const p = document.createElement('p');
  77. const pTag = document.createElement('p');
  78. p.classList.add('article-preview');
  79. p.textContent = 'No articles are here... yet.';
  80. pTag.textContent = 'No tags are here... yet.';
  81. articleList.appendChild(p);
  82. tagList.appendChild(pTag);
  83. }
  84. }
  85. const focusFeed = () => {
  86. // 이미 포커싱된 탭이라면 중지
  87. const parentNode = event.target.parentNode;
  88. if (parentNode.classList.contains('active')) {
  89. return;
  90. }
  91. const unFocusedFeed = parentNode;
  92. const currentFeed = (unFocusedFeed.id === 'global-feed') ? window.document.querySelector('#your-feed') :
  93. window.document.querySelector('#global-feed');
  94. unFocusedFeed.classList.add('active');
  95. if ("${ssIsLogin}" === "true") {
  96. currentFeed.classList.remove('active');
  97. }
  98. // your-feed 또는 global-feed 클릭시 tag-feed 없애기
  99. const tagToggle = document.querySelector('#tag-feed');
  100. const spanTag = document.querySelector('#tag-feed span');
  101. if (tagToggle.classList.contains('active')) {
  102. tagToggle.classList.remove('active');
  103. spanTag.textContent = null;
  104. document.querySelector('.fa-hashtag').style.display = 'none';
  105. }
  106. // targetFeed 초기화
  107. targetFeed = unFocusedFeed;
  108. setLoading('on');
  109. // 태그와 게시글 초기화
  110. tagArray = [];
  111. while(tagList.firstChild) {
  112. tagList.firstChild.remove();
  113. }
  114. articleList = document.querySelector('#article-list');
  115. while (articleList.firstChild) {
  116. articleList.firstChild.remove();
  117. }
  118. params = {
  119. articleId: -1,
  120. feed: targetFeed.id
  121. };
  122. query = Object.keys(params)
  123. .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
  124. .join('&');
  125. url = '/article/page?' + query;
  126. fetch(url, options)
  127. .then(response => response.json())
  128. .then(json => {
  129. const {
  130. articles,
  131. paging
  132. } = json;
  133. loadArticle(articles);
  134. nextPageLoad(articles, paging);
  135. });
  136. filterArticle({
  137. 'clickedFeed': unFocusedFeed.id
  138. })
  139. setTimeout(() => {
  140. setLoading('off');
  141. noContent();
  142. }, 2000);
  143. }
  144. // 좋아요 버튼
  145. const favoriteBtn = (indexNumber) => {
  146. const favoriteNum = event.target.querySelector('.count').textContent;
  147. const favorite = event.target.querySelector('.favorite-btn');
  148. let counts = parseInt(favoriteNum);
  149. let newCounts = counts + 1;
  150. // 로그인한 경우에만 실행
  151. if ("${ssIsLogin}" === "true") {
  152. if (event.target.classList.contains('active')) { // 이미 좋아요 버튼을 클릭한 경우
  153. event.target.querySelector('.count').textContent = counts - 1;
  154. event.target.classList.remove('active');
  155. fetch('/article/' + indexNumber + '/favorite', {
  156. method: 'DELETE'
  157. })
  158. .then(response => {
  159. if (response.status === 200) {
  160. return;
  161. }
  162. });
  163. } else { // 좋아요 버튼을 클릭하지 않은 경우
  164. event.target.querySelector('.count').textContent = newCounts;
  165. event.target.classList.add('active');
  166. fetch('/article/' + indexNumber + '/favorite', {
  167. body: JSON.stringify({
  168. articleId: indexNumber,
  169. userId: "${ssId}",
  170. created: new Date()
  171. }),
  172. method: 'POST',
  173. headers: {
  174. "Content-Type": "application/json"
  175. }
  176. })
  177. .then(response => {
  178. if (response.status === 201) {
  179. return;
  180. }
  181. })
  182. }
  183. } else {
  184. location.href = "/user/signin"
  185. }
  186. }
  187. // 태그 정렬 https://curryyou.tistory.com/229
  188. const getSortedArr = (array) => {
  189. // 출현 빈도 구하기
  190. const counts = array.reduce((pv, cv) => {
  191. pv[cv] = (pv[cv] || 0) + 1;
  192. return pv;
  193. }, {});
  194. // 배열 생성 => [ [ key: 개수 ], [ key: 개수 ], ...]
  195. const result = [];
  196. for (let key in counts) {
  197. result.push([key, counts[key]]);
  198. };
  199. // 빈도별로 정렬
  200. result.sort((first, second) => {
  201. return second[1] - first[1];
  202. });
  203. // 최대 10개까지만 표시
  204. return result.slice(0, 10);
  205. }
  206. // 현재 클릭한 태그 표시
  207. const focusTag = () => {
  208. let currentValue = document.querySelector('.tag.active');
  209. if (currentValue !== null) {
  210. currentValue.classList.remove('active');
  211. }
  212. event.target.classList.add('active');
  213. }
  214. // 게시글 및 태그 표시
  215. const loadArticle = (articles) => {
  216. articles.forEach(article => {
  217. console.log(JSON.stringify(article))
  218. const domParser = new DOMParser();
  219. // 게시글 정보 표시
  220. const domStrArticleMeta =
  221. `
  222. <div class="article-meta">
  223. <div class="metadata">
  224. <a href="/user/\${article.writerId}" class="profile-link">
  225. <img src="/resources/images/avatar.png" alt="avatar">
  226. </a>
  227. <div class="article-info">
  228. <a href="/user/\${article.writerId}" class="name"></a>
  229. <span class="date"></span>
  230. </div>
  231. </div>
  232. <div>
  233. <button class="favorite-btn" onclick="favoriteBtn(\${article.id})">
  234. <i class="fas fa-heart"></i>
  235. <span class="count">\${article.favoriteNum}</span>
  236. </button>
  237. </div>
  238. </div>
  239. `;
  240. const divArticleMeta = domParser.parseFromString(domStrArticleMeta, 'text/html').body
  241. .firstChild;
  242. divArticleMeta.querySelector('.name').textContent = article.writerName;
  243. divArticleMeta.querySelector('.date').textContent = new Date(article.created).toLocaleString();
  244. // 좋아요한 게시글의 경우
  245. if (article.favorite) {
  246. divArticleMeta.querySelector('.favorite-btn').classList.add('active');
  247. }
  248. // 게시글 내용 및 게시글태그 표시
  249. const domStrPreviewLink =
  250. `
  251. <a href="/article/\${article.id}" class="preview-link">
  252. <h1 class="preview-title"></h1>
  253. <p></p>
  254. <div class="tag-data">
  255. <span>Read more...</span>
  256. <ul class = "tag-list">
  257. </ul>
  258. </div>
  259. </a>
  260. `;
  261. const aPreviewLink = domParser.parseFromString(domStrPreviewLink, 'text/html').body.firstChild;
  262. aPreviewLink.querySelector('.preview-title').textContent = article.title;
  263. aPreviewLink.querySelector('p').textContent = article.subtitle;
  264. const ul = aPreviewLink.querySelector('.tag-list');
  265. if (article.tags !== '') {
  266. article.tags.split(',').forEach(tag => {
  267. if (!tagSet.has(tag)) {
  268. tagSet.add(tag);
  269. }
  270. const li = window.document.createElement('li');
  271. li.classList.add('tag');
  272. li.textContent = tag;
  273. ul.appendChild(li);
  274. })
  275. }
  276. // article 완성
  277. const articlePreview = window.document.createElement('article');
  278. articlePreview.classList.add('article-preview');
  279. articlePreview.appendChild(divArticleMeta);
  280. articlePreview.appendChild(aPreviewLink);
  281. if (targetFeed.id === 'your-feed') {
  282. articlePreview.style.display = ("${ssUsername}" === article.writerName) ? 'block' : 'none';
  283. }
  284. articleList.appendChild(articlePreview);
  285. // Popular tag 배열 생성
  286. let tagsArray = article.tags.split(',');
  287. const tagList = document.querySelector('#tag-list');
  288. while (tagList.firstChild) {
  289. tagList.firstChild.remove();
  290. }
  291. tagsArray.forEach(tag => {
  292. if (tag !== '') {
  293. tagArray.push(tag);
  294. }
  295. })
  296. });
  297. const tags = Array.from(tagSet);
  298. const tagList = window.document.querySelector('#tag-list');
  299. // Popular tag 표시
  300. getSortedArr(tagArray).forEach(tag => {
  301. const a = document.createElement('a');
  302. const tagString = tag.join(',');
  303. const tagValue = tagString.substring(0, tagString.lastIndexOf(','));
  304. const tagCount = tagString.substring(tagString.lastIndexOf(',') + 1);
  305. a.classList.add("tag");
  306. a.textContent = `\${tagValue} (\${tagCount})`
  307. a.onclick = (event) => {
  308. const tagToggle = document.querySelector('#tag-feed');
  309. const yourToggle = document.querySelector('#your-feed');
  310. const globalToggle = document.querySelector('#global-feed');
  311. const moreBtn = document.querySelector('.more-button');
  312. const spanTag = document.querySelector('#tag-feed span');
  313. // 해당 태그의 게시물 표시
  314. filterArticle({
  315. 'clickedTag': tagValue
  316. });
  317. // 해당 태그의 토글버튼 생성
  318. document.querySelector('.fa-hashtag').style.display = 'block';
  319. spanTag.textContent = '\u00A0' + tagValue;
  320. tagToggle.classList.add('active');
  321. globalToggle.classList.remove('active');
  322. // 로그인한 경우에만 your-feed가 생기므로 조건문 처리
  323. if ("${ssIsLogin}" === "true") {
  324. yourToggle.classList.remove('active');
  325. }
  326. // more 버튼 삭제
  327. if (moreBtn !== null) {
  328. moreBtn.remove();
  329. }
  330. }
  331. tagList.appendChild(a);
  332. });
  333. }
  334. // 다음페이지 로드
  335. const nextPageLoad = (articles, paging) => {
  336. // 더 조회될 수 있는 게시글이 있는지 여부
  337. if (paging.isNext) {
  338. const moreButton = document.createElement('button');
  339. const articleList = document.querySelector('#article-list');
  340. const loading = document.createElement('div');
  341. moreButton.textContent = '더보기';
  342. moreButton.classList.add('more-button');
  343. loading.setAttribute('id', 'loading');
  344. articleList.appendChild(moreButton);
  345. // more 버튼 클릭 시 다음페이지 게시물을 가져온다
  346. moreButton.onclick = () => {
  347. articleList.removeChild(moreButton);
  348. articleList.appendChild(loading); // 로딩 생성
  349. // 현재 페이지의 마지막 게시글 id
  350. params['articleId'] = articles[articles.length - 1].id;
  351. query = Object.keys(params)
  352. .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
  353. .join('&');
  354. url = '/article/page?' + query;
  355. fetch(url, options)
  356. .then(response => response.json())
  357. .then(json => {
  358. const {
  359. articles,
  360. paging
  361. } = json;
  362. setTimeout(() => {
  363. articleList.removeChild(loading);
  364. loadArticle(articles);
  365. nextPageLoad(articles, paging);
  366. }, 1000);
  367. })
  368. }
  369. }
  370. }
  371. window.onload = (event) => {
  372. ssIsLogin = "${ssIsLogin}";
  373. targetFeed = (ssIsLogin === "true") ? window.document.querySelector('#your-feed') : window.document
  374. .querySelector('#global-feed');
  375. targetFeed.classList.add('active');
  376. loadingTag = window.document.querySelector('#tag-loading');
  377. loadingArticle = window.document.querySelector('#article-loading');
  378. articleList = window.document.querySelector('#article-list');
  379. noTag = window.document.querySelector('#no-tag');
  380. tagList = window.document.querySelector('#tag-list');
  381. setLoading('on');
  382. //fetch api call
  383. fetch(url, options)
  384. .then(response => response.json())
  385. .then(json => {
  386. const {
  387. articles,
  388. paging
  389. } = json;
  390. loadArticle(articles);
  391. nextPageLoad(articles, paging);
  392. // hide loadings
  393. setTimeout(() => {
  394. setLoading('off');
  395. noContent();
  396. }, 2000);
  397. });
  398. }
  399. </script>
  400. <style>
  401. </style>
  402. </head>
  403. <body>
  404. <jsp:include page="/WEB-INF/jsp/include/header.jsp"></jsp:include>
  405. <!-- home-page content -->
  406. <div class="home-page">
  407. <!-- Banner -->
  408. <section class="banner">
  409. <div class="container">
  410. <h1 class="banner-title">conduit</h1>
  411. <p class="banner-description">A place to share your knowledge.</p>
  412. </div>
  413. </section>
  414. <!-- Main - Contents & aside-->
  415. <div class="container main">
  416. <div class="row">
  417. <div class="col-9">
  418. <!-- 토글 버튼으로 피드 내용 보기 -->
  419. <!-- toggle -->
  420. <div class="toggle">
  421. <ul class="nav">
  422. <!-- 로그인 상태인 경우에만 your feed on -->
  423. <c:if test="${ssIsLogin eq true}">
  424. <li id="your-feed" class="nav-item">
  425. <a href="javascript:void(0);" onclick="focusFeed()">Your
  426. Feed</a>
  427. </li>
  428. </c:if>
  429. <li id="global-feed" class="nav-item">
  430. <a href="javascript:void(0);" onclick="focusFeed()">Global
  431. Feed</a>
  432. </li>
  433. <li id="tag-feed" class="nav-item">
  434. <a href="javascript:void(0);">
  435. <i class="fas fa-hashtag" style="display: none;"></i>
  436. <span></span>
  437. </a>
  438. </li>
  439. </ul>
  440. </div>
  441. <!-- content list -->
  442. <div class="article">
  443. <!-- loading -->
  444. <article id="article-loading">
  445. loading articles...
  446. </article>
  447. <!-- article list -->
  448. <div id="article-list" style="display: none;"></div>
  449. </div>
  450. </div>
  451. <!-- aside -->
  452. <div class="col-3">
  453. <div class="aside">
  454. <p>Popular Tags</p>
  455. <!-- loading -->
  456. <div id="tag-loading">
  457. loading tags...
  458. </div>
  459. <!-- 태그 -->
  460. <div id="tag-list" class="tag-list" style="display: none;" onclick="focusTag()">
  461. </div>
  462. </div>
  463. </div>
  464. </div>
  465. </div>
  466. </div>
  467. </body>
  468. </html>