概要
この記事は、昔自分が独学で自作した掲示板プログラムを備忘録兼、誰かの役に立てばと思い記述しました
作成した掲示板サイト
スレッドを作成し、作成されたスレッドに対してそれぞれコメントを投稿できるシンプルな掲示板です。
工夫した部分
- コメントの投稿者名を指定する
- 投稿者名、コメント数の文字数を制限して、異常があれば投稿せず警告を表示する
- コメントの投稿時間、スレッドの更新時間、コメント数を表示する
- スレッドが作成されるたびに各スレッドの識別子を作成し、その識別子から各スレッドへ遷移する
- コメント部分と背景で色分けをしている
- スレッド作成時、確認ダイアログを表示する
- スレッド数が10個以上になるとページングで表示する
- スレッド内でページ最上部、最下部に移動するボタン
- スレッド作成ボタンとコメント書き込むボタンの表示が、カーソルが上にあるとボタンが押されたようにする
データベース
使用したツール等
- Visual Studio Code
- PostgreSQL 14
- Java 17
- Gradle 7.6.1
- SpringBoot 3.1.0
- Bootstrap 5.0.2
- ConoHa VPS
- Ubuntu 22.04
- TeraTerm
作成したコード
概要
MVCモデルで作成している。
Thread.java、Comments.java、ThreadCommentForm.java、ThreadCreateForm.java、ViewHogeObject.java、ViewIndexObject.javaがModel部分に相当する。
index.html、thread-create.html、thread-hoge.html、about.htmlがView部分に相当する。
indexController.java、threadCreateController.java、threadHogeController.java、hermit03Application.javaがController部分に相当する。
hermit03Application.javaはメインメソッドも兼ねている。
CommentsRepository.javaとThreadRepository.javaはリポジトリーである。
securityconfig.javaはSpring Securityの設定ファイルである。
Java
Thread.java
package com.example.hermit03;
import java.sql.Timestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
//threadテーブル用クラス
@Data
@Entity
@Table(name = "thread")
public class Thread {
// ID
@Id
@Column
private String uuid;
// スレッド作成者
@Column(length = 20, nullable = true)
private String creater;
// スレッドタイトル
@Column(length = 30, nullable = false)
private String title;
// スレッドコメント数
@Column(nullable = false)
private Integer count;
// スレッド作成日時
@Column(nullable = false)
private Timestamp createdatetime;
// スレッド更新日時
@Column(nullable = false)
private Timestamp updatadatetime;
// 最初のコメント
@Column(length = 500, nullable = true)
private String firstcomment;
}
Comments.java
package com.example.hermit03;
import java.sql.Timestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
//Commnetsテーブル用クラス
@Data
@Entity
@Table(name = "comments")
public class Comments {
// ID
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column
private long id;
// スレッドUUID
@Column(nullable = false)
private String threaduuid;
// コメント投稿者
@Column(length = 20, nullable = true)
private String commentname;
// コメント内容
@Column(length = 500, nullable = false)
private String comment;
// スレッド投稿日時
@Column(nullable = false)
private Timestamp createdatetime;
}
ThreadCommentForm.java
package com.example.hermit03;
import jakarta.persistence.Column;
import jakarta.validation.constraints.Size;
import lombok.Data;
//コメント投稿フォーム用クラス
@Data
public class ThreadCommentForm {
//投稿者名
@Column(nullable = true)
@Size(min=0, max=20,message = "投稿者名は20文字以内で入力してください")
private String commentname;
//投稿内容
@Column(nullable = true)
@Size(min=1, max=500,message = "投稿内容は500文字以内で入力してください")
private String comment;
}
ThreadCreateForm.java
package com.example.hermit03;
import jakarta.persistence.Column;
import jakarta.validation.constraints.Size;
import lombok.Data;
//スレッド作成フォーム用クラス
@Data
public class ThreadCreateForm {
//投稿者名
@Column(nullable = true)
@Size(min=0, max=30,message = "投稿者名は20文字以内で入力してください")
private String creater;
//スレッドタイトル
@Column(nullable = false)
@Size(min=1, max=30,message = "スレッドタイトルは30文字以内で入力してください")
private String threadtitle;
//最初の投稿内容
@Column(nullable = false)
@Size(min=1, max=500,message = "投稿内容は500文字以内で入力してください")
private String firstcomment;
}
ViewHogeObject.java
package com.example.hermit03;
import lombok.Data;
//コメントビュー用クラス
@Data
public class ViewHogeObject {
private Integer comment_id; //コメントID
private String creater; //投稿者名
private String createdatetime; //作成日時
private String comment; //投稿内容
}
ViewIndexObject.java
package com.example.hermit03;
import lombok.Data;
//indexビュー用クラス
@Data
public class ViewIndexObject {
private String uuid; //スレッドID
private String title; //スレッドタイトル
private int count; //スレッドコメント数
private String updatadatetime; //更新日時
}
hermit03Application.java
package com.example.hermit03;
import java.util.concurrent.CompletableFuture;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
//メインクラスと/aboutコントローラー
@SpringBootApplication
@Controller
@EnableAsync
public class hermit03Application {
//メインメソッド
public static void main(String[] args) {
SpringApplication.run(hermit03Application.class, args);
}
//aboutへのGETマッピング
@RequestMapping("/about")
@Async
public CompletableFuture<String> about() {
return CompletableFuture.completedFuture("about");
}
}
indexController.java
package com.example.hermit03;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.example.hermit03.repositories.ThreadRepository;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.PageableDefault;
import org.springframework.scheduling.annotation.Async;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
//indexコントローラークラス
@Controller
public class indexController {
@Autowired
ThreadRepository threadRepository;
//inde画面へのGETマッピング
@RequestMapping(value = "/", method = RequestMethod.GET)
@Async
public CompletableFuture<String> index(Model model, @PageableDefault(page = 0, size = 10, direction = Direction.DESC, sort = {
"updatadatetime" }) Pageable pageable) {
Page<Thread> page = threadRepository.findAll(pageable);
List<Thread> pagelist = page.getContent();
List<ViewIndexObject> vio = new ArrayList<ViewIndexObject>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/M/d/HH:mm");
// indexビュー用のクラスへ変換
for (Thread thread : pagelist) {
ViewIndexObject viofor = new ViewIndexObject();
viofor.setUuid(thread.getUuid());
viofor.setTitle(thread.getTitle());
viofor.setCount(thread.getCount());
viofor.setUpdatadatetime(sdf.format(thread.getUpdatadatetime()));
vio.add(viofor);
}
model.addAttribute("page", page);
model.addAttribute("vio", vio);
return CompletableFuture.completedFuture("index");
}
}
threadCreateController.java
package com.example.hermit03;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.PageableDefault;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.example.hermit03.repositories.CommentsRepository;
import com.example.hermit03.repositories.ThreadRepository;
import org.springframework.transaction.annotation.Transactional;
//スレッド作成画面のコントローラークラス
@Controller
public class threadCreateController {
@Autowired
ThreadRepository threadRepository;
@Autowired
CommentsRepository commentsRepository;
//スレッド作成画面のGETマッピング
@RequestMapping(value = "/thread-create", method = RequestMethod.GET)
@Async
public CompletableFuture<String> threadCreate(Model model, @ModelAttribute ThreadCreateForm tcf) {
model.addAttribute("tcf", tcf);
return CompletableFuture.completedFuture("thread-create");
}
//スレッド作成画面のPOSTマッピング
@RequestMapping(value = "/thread-create", method = RequestMethod.POST)
@Transactional
@Async
public CompletableFuture<String> index(Model model, @PageableDefault(page = 0, size = 10, direction = Direction.DESC, sort = {
"updatadatetime" }) Pageable pageable, @Validated @ModelAttribute("tcf") ThreadCreateForm tcf,
BindingResult result) {
//入力エラーがあるかチェック
if (result.hasErrors() == true) {
model.addAttribute("tcf", tcf);
return CompletableFuture.completedFuture("thread-create");
}
//スレッドテーブルとコメントテーブルにレコードを作成
UUID uuid = UUID.randomUUID();
String uuidstr = uuid.toString();
Thread thread = new Thread();
Comments comments = new Comments();
Timestamp time = new Timestamp(System.currentTimeMillis());
thread.setUuid(uuidstr);
if (tcf.getCreater() == "") {
thread.setCreater("名無しさん");
comments.setCommentname("名無しさん");
} else {
thread.setCreater(tcf.getCreater());
comments.setCommentname(tcf.getCreater());
}
thread.setTitle(tcf.getThreadtitle());
thread.setCount(1);
thread.setFirstcomment(tcf.getFirstcomment());
thread.setCreatedatetime(time);
thread.setUpdatadatetime(time);
threadRepository.saveAndFlush(thread);
comments.setThreaduuid(uuidstr);
comments.setComment(tcf.getFirstcomment());
comments.setCreatedatetime(time);
commentsRepository.saveAndFlush(comments);
Page<Thread> page = threadRepository.findAll(pageable);
List<Thread> pagelist = page.getContent();
List<ViewIndexObject> vio = new ArrayList<ViewIndexObject>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/M/d/HH:mm");
// indexビュー用のクラスへ変換
for (Thread pgli : pagelist) {
ViewIndexObject viofor = new ViewIndexObject();
viofor.setTitle(pgli.getTitle());
viofor.setCount(pgli.getCount());
viofor.setUpdatadatetime(sdf.format(pgli.getUpdatadatetime()));
vio.add(viofor);
}
model.addAttribute("page", page);
model.addAttribute("vio", vio);
return CompletableFuture.completedFuture("redirect:/");
}
}
threadHogeController.java
package com.example.hermit03;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.example.hermit03.repositories.CommentsRepository;
import com.example.hermit03.repositories.ThreadRepository;
//スレッド画面のコントローラークラス
@Controller
public class threadHogeController {
@Autowired
ThreadRepository threadRepository;
@Autowired
CommentsRepository commentsRepository;
//各スレッドのGETマッピング
@RequestMapping(value = "/thread/{threaduuid}", method = RequestMethod.GET)
@Async
public CompletableFuture<String> threadhoge(@PathVariable("threaduuid") String threaduuid, Model model,
@ModelAttribute ThreadCommentForm tcf) {
Optional<Thread> thread = threadRepository.findById(threaduuid);
List<Comments> comments = commentsRepository.findByThreaduuid(threaduuid);
List<ViewHogeObject> vho = new ArrayList<ViewHogeObject>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/M/d/ HH:mm:ss");
int comment_id = 1;
for (Comments comment : comments) {
ViewHogeObject vhofor = new ViewHogeObject();
vhofor.setComment_id(comment_id);
vhofor.setComment(comment.getComment());
vhofor.setCreatedatetime(sdf.format(comment.getCreatedatetime()));
vhofor.setCreater(comment.getCommentname());
vho.add(vhofor);
comment_id++;
}
model.addAttribute("tcf", tcf);
model.addAttribute("vho", vho);
model.addAttribute("title", thread.get().getTitle());
model.addAttribute("threaduuid", thread.get().getUuid());
return CompletableFuture.completedFuture("thread-hoge");
}
//各スレッド画面のPOSTマッピング
@RequestMapping(value = "/thread/{threaduuid}", method = RequestMethod.POST)
@Transactional
@Async
public CompletableFuture<String> threadhogecomment(Model model, @PathVariable("threaduuid") String threaduuid,
@Validated @ModelAttribute("tcf") ThreadCommentForm tcf, BindingResult result) {
//入力エラーがあるかチェック
if (result.hasErrors() == true) {
Optional<Thread> thread = threadRepository.findById(threaduuid);
List<Comments> comments = commentsRepository.findByThreaduuid(threaduuid);
List<ViewHogeObject> vho = new ArrayList<ViewHogeObject>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/M/d/ HH:mm:ss");
int comment_id = 1;
for (Comments comment : comments) {
ViewHogeObject vhofor = new ViewHogeObject();
vhofor.setComment_id(comment_id);
vhofor.setComment(comment.getComment());
vhofor.setCreatedatetime(sdf.format(comment.getCreatedatetime()));
vhofor.setCreater(comment.getCommentname());
vho.add(vhofor);
comment_id++;
}
model.addAttribute("tcf", tcf);
model.addAttribute("vho", vho);
model.addAttribute("title", thread.get().getTitle());
model.addAttribute("threaduuid", thread.get().getUuid());
return CompletableFuture.completedFuture("thread-hoge");
}
//コメントテーブルにレコードを作成、スレッドテーブルのデータを更新
Comments comments = new Comments();
Timestamp time = new Timestamp(System.currentTimeMillis());
comments.setComment(tcf.getComment());
if (tcf.getCommentname() == "" || tcf.getCommentname() == null) {
comments.setCommentname("名無しさん");
} else {
comments.setCommentname(tcf.getCommentname());
}
comments.setCreatedatetime(time);
comments.setThreaduuid(threaduuid);
commentsRepository.saveAndFlush(comments);
Optional<Thread> thread = threadRepository.findById(threaduuid);
Thread updatathread = new Thread();
updatathread.setUuid(threaduuid);
updatathread.setCount((thread.get().getCount() + 1));
updatathread.setUpdatadatetime(time);
updatathread.setCreatedatetime(thread.get().getCreatedatetime());
updatathread.setFirstcomment(thread.get().getFirstcomment());
updatathread.setTitle(thread.get().getTitle());
updatathread.setCreater(thread.get().getCreater());
threadRepository.saveAndFlush(updatathread);
return CompletableFuture.completedFuture("redirect:/thread/{threaduuid}");
}
}
CommentsRepository.java
package com.example.hermit03.repositories;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.hermit03.Comments;
public interface CommentsRepository extends JpaRepository<Comments,Long>{
List<Comments> findByThreaduuid(String threaduuid);
}
ThreadRepository.java
package com.example.hermit03.repositories;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.hermit03.Thread;
public interface ThreadRepository extends JpaRepository<Thread, String> {
Page<Thread> findAll(Pageable pageable);
}
securityconfig.java
package com.example.hermit03;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class securityconfig {
@Bean
public SecurityFilterChain filterchain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(authorize ->{
authorize
.anyRequest().permitAll();
});
return http.build();
}
}
HTML
index.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charaset="UTF-8">
<meta name="description" content="掲示板">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" th:href="@{/css/ress.min.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/hermitstyle1.css}">
<title>HerMit</title>
</head>
<body>
<header>
<a href="/"><img class="logo" th:src="@{/img/hermit-logo.png}" alt="HerMit"></a>
<nav>
<ul class="header-nav">
<li class="border border-primary border-2 rounded"><a href="/about">HerMitとは</a>
</li>
<li class="border border-primary border-2 rounded"><a
href="/thread-create">スレッドを作る</a></li>
</ul>
</nav>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-2 left-col">
</div>
<div class="main-area col-8">
<div th:each="vio : ${vio}" th:object="${vio}">
<a th:href="@{/thread/{uuid}(uuid=${vio.uuid})}">
<h2>
[[${vio.title}]]([[${vio.count}]])
<span>最終更新:[[${vio.updatadatetime}]]</span>
</h2>
</a>
</div>
<!-- ページング↓ -->
<div>
<ul class="pageing">
<li>
<span th:if="${page.first}"><<前</span>
<a th:if="${!page.first}" th:href="@{/(page = ${page.number} - 1)}">
<<前
</a>
</li>
<li th:each="i : ${#numbers.sequence(0, page.totalPages - 1)}">
<span th:if="${i} == ${page.number}" th:text="${i + 1}">1</span>
<a th:if="${i} != ${page.number}" th:href="@{/(page = ${i})}">
<span th:text="${i+1}">1</span>
</a>
</li>
<li>
<span th:if="${page.last}">次>></span>
<a th:if="${!page.last}" th:href="@{/(page = (${page.number} + 1))}">
次>>
</a>
</li>
</ul>
</div>
<!-- ページング↑ -->
</div>
<div class="col-2 right-col">
</div>
</div>
</div>
<footer>
<ul class="footer-nav">
<li><a href="/about">HerMitとは</a></li>
<li>|</li>
<li><a href="/about#terms">利用規約</a></li>
<li>|</li>
<li><a href="/about#inquiry">お問い合わせ</a></li>
</ul>
<p>© 2023 HerMit</p>
</footer>
<script th:src="@{/js/bootstrap.bundle.min.js}"></script>
</body>
thread-create.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charaset="UTF-8">
<meta name="description" content="掲示板">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" th:href="@{/css/ress.min.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/hermitstyle1.css}">
<title>HerMit</title>
</head>
<body>
<header>
<a href="/"><img class="logo" th:src="@{/img/hermit-logo.png}" alt="HerMit"></a>
<nav>
<ul class="header-nav">
<li class="border border-primary border-2 rounded"><a href="/about">HerMitとは</a></li>
<li class="border border-primary border-2 rounded"><a href="/thread-create">スレッドを作る</a></li>
</ul>
</nav>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-2 left-col">
</div>
<div class="main-area col-8">
<form class="thread-create" action="/thread-create" method="post" th:object="${tcf}" onSubmit="return check()">
<div class="form-thread-create mb-3">
<label for="FormName" class="form-label">・投稿者名(省略可・20文字以内)</label>
<div class="validation" th:if="${#fields.hasErrors('creater')}" th:errors="*{creater}"></div>
<input type="text" class="form-control bg-secondary-sub border-dark" id="FormName"
name="creater" placeholder="投稿者名を入力" th:field="*{creater}" />
</div>
<div class="form-thread-create mb-3">
<label for="Thread-title" class="form-label ">・スレッドタイトル(必須・30文字以内)</label>
<div class="validation" th:if="${#fields.hasErrors('threadtitle')}" th:errors="*{threadtitle}"></div>
<input type="text" class="form-control bg-secondary-sub border-dark" id="Thread-title"
name="threadtitle" placeholder="スレッドタイトルを入力" th:field="*{threadtitle}" >
</div>
<div class="form-thread-create mb-3">
<label for="FormMessage" class="form-label">・最初の投稿(必須・500文字以内)</label>
<div class="validation" th:if="${#fields.hasErrors('firstcomment')}" th:errors="*{firstcomment}"></div>
<textarea class="form-control bg-secondary-sub border-dark" id="FormMessage" rows="5"
name="firstcomment" placeholder="最初の投稿を入力" th:field="*{firstcomment}" ></textarea>
</div>
<div class="create-buttton d-grid gap-2 col-6 mx-auto">
<button class="btn btn-c btn--green btn--cubic">スレッド作成</button>
</div>
</form>
</div>
<div class="col-2 right-col">
</div>
<footer>
<ul class="footer-nav">
<li><a href="/about">HerMitとは</a></li>
<li>|</li>
<li><a href="/about#terms">利用規約</a></li>
<li>|</li>
<li><a href="/about#inquiry">お問い合わせ</a></li>
</ul>
<p>© 2023 HerMit</p>
</footer>
<script th:src="@{/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/hermitscript.js}"></script>
</body>
</html>
thread-hoge.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charaset="UTF-8">
<meta name="description" content="掲示板">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" th:href="@{/css/ress.min.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/hermitstyle1.css}">
<title>HerMit</title>
</head>
<body>
<header>
<a href="/"><img class="logo" th:src="@{/img/hermit-logo.png}" alt="HerMit"></a>
<nav>
<ul class="header-nav">
<li class="border border-primary border-2 rounded"><a href="/about">HerMitとは</a></li>
<li class="border border-primary border-2 rounded"><a href="/thread-create">スレッドを作る</a></li>
</ul>
</nav>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-2 left-col">
</div>
<div class="main-area col-8" id="top">
<a href="#bottom" class="btn btn-primary rounded-pill jump">1番下へジャンプ</a>
<h1 th:text="${title}"></h1>
<div th:each="vho : ${vho}" th:object="${vho}">
<p>[[${vho.comment_id}]]:[[${vho.creater}]] [[${vho.createdatetime}]]<br>[[${vho.comment}]]</p>
</div>
<a href="#top" class="btn btn-primary rounded-pill jump"id="bottom">1番上へジャンプ</a>
<form class="thread-comment" th:action="@{/thread/{threaduuid}(threaduuid=${threaduuid})}" method="post"
th:object="${tcf}">
<div class="form-thread-create mb-3">
<label for="FormName" class="form-label">・投稿者名(省略可・20文字以内)</label>
<div class="validation" th:if="${#fields.hasErrors('commentname')}" th:errors="*{commentname}"></div>
<input type="text" class="form-control bg-secondary-sub border-dark" id="FormName"
name="commentname" placeholder="投稿者名を入力" th:field="*{commentname}">
</div>
<div class="form-thread-create mb-3">
<label for="FormMessage" class="form-label">・投稿内容(必須・500文字以内)</label>
<div class="validation" th:if="${#fields.hasErrors('comment')}" th:errors="*{comment}"></div>
<textarea class="form-control bg-secondary-sub border-dark" id="FormMessage" rows="5"
name="comment" placeholder="投稿内容を入力" th:field="*{comment}"></textarea>
</div>
<div class="create-buttton d-grid gap-2 col-6 mx-auto">
<button class="btn btn-c btn--green btn--cubic">書き込む</button>
</div>
</form>
</div>
<div class="col-2 right-col">
</div>
</div>
</div>
<footer>
<ul class="footer-nav">
<li><a href="/about">HerMitとは</a></li>
<li>|</li>
<li><a href="/about#terms">利用規約</a></li>
<li>|</li>
<li><a href="/about#inquiry">お問い合わせ</a></li>
</ul>
<p>© 2023 HerMit</p>
</footer>
<script th:src="@{/js/bootstrap.bundle.min.js}"></script>
</body>
</html>
about.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charaset="UTF-8">
<meta name="description" content="掲示板">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" th:href="@{/css/ress.min.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/hermitstyle1.css}">
<title>HerMit</title>
</head>
<body>
<header>
<a href="/"><img class="logo" th:src="@{/img/hermit-logo.png}" alt="HerMit"></a>
<nav>
<ul class="header-nav">
<li class="border border-primary border-2 rounded"><a href="/about">HerMitとは</a></li>
<li class="border border-primary border-2 rounded"><a href="/thread-create">スレッドを作る</a></li>
</ul>
</nav>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-2 left-col">
</div>
<div class="col-8">
<div class="main-area">
<h1>HerMitとは</h1>
<p>HerMitとは個人が開発したシンプルな匿名掲示板です。誰でも利用できます。
<br>バグが多々あると思いますが、その場合は下記の<a href="#inquiry">お問い合わせ</a>までお願いします。(非同期処理非対応です)
</p>
<h1 id="terms">利用規約</h1>
<div class="greenback">
<p>この利用規約(以下、「本規約」といいます。)は、このウェブサイト「HerMit」上で 提供するサービス(以下、「本サービス」といいます。)の利用条件を、
本サービスの運営者(以下、「当運営者」といいます。)が定めるものです。 ユーザーの皆さま(以下、「ユーザー」といいます。)は、本規約に従って、本サービスを利用する必要があります。
</p>
<h5>第1条(規約の同意)</h5>
<p>
ユーザーは、本サービスを利用することにより、本規約の全ての記載内容についてに同意したものとみなされます。</p>
<h5>第2条(規約の適用)</h5>
<p>
本規約は、ユーザーと当運営者との間の本サービスの利用に関わる一切の関係に適用されるものとします。
当社は本サービスに関し、本規約のほか、ご利用にあたってのルール等、各種の定め(以下、「個別規定」といいます。)をすることがあります。これら個別規定はその名称のいかんに関わらず、本規約の一部を構成するものとします。
本規約の規定が前条の個別規定の規定と矛盾する場合には、個別規定において特段の定めなき限り、個別規定の規定が優先されるものとします。
</p>
<h5>第3条(禁止事項)</h5>
<p>
ユーザーは、本サービスの利用にあたり、以下の投稿や行為等をしてはなりません。
禁止事項に違反した場合、当運営者は投稿削除や投稿禁止,アクセス禁止等の制限を行う他、 法的責任を追求するなど不利益な措置を採ることがあります。
また、権利者から発信者情報開示請求があった場合、法令に則って開示することがあります。
・法令、公序良俗に反するもの(違法な行為を誘発する恐れのある投稿等を含む)。
・第三者の権利を侵害したり、誹謗中傷したりするもの。
・第三者のプライバシーに関する事項や差別的表現を含むもの。
・有害なコンピュータープログラム等を内容とするもの。
・広告、宣伝目的のもの(リンク先が広告・宣伝目的のものを含む)。
・本サービスの運営を妨害するもの(荒らし行為を含む)。
・その他、当運営者が不適切と判断するもの。</p>
<h5>第4条(利用料金)</h5>
<p>
本サービスは無料とします。</p>
<h5>第5条(本サービスの提供の停止等)</h5>
<p>
当運営者は、以下のいずれかの事由があると判断した場合、ユーザーに事前に通知することなく本サービスの全部または一部の提供を停止または中断することができるものとします。
本サービスにかかるコンピュータシステムの保守点検または更新を行う場合
地震、落雷、火災、停電または天災などの不可抗力により、本サービスの提供が困難となった場合
コンピュータまたは通信回線等が事故により停止した場合
その他、当運営者が本サービスの提供が困難と判断した場合
当運営者は、本サービスの提供の停止または中断により、ユーザーまたは第三者が被ったいかなる不利益または損害についても、一切の責任を負わないものとします。
</p>
<h5>第6条(知的財産権)</h5>
<p>
ユーザーは、自ら著作権等の必要な知的財産権を有するか、または必要な権利者の許諾を得た文章、画像や映像等の情報に関してのみ、本サービスを利用し、投稿することができるものとします。
ユーザーが本サービスを利用して投稿した内容の著作権については、当該ユーザーその他既存の権利者に留保されるものとします。
当運営者及び当運営者が許諾した者は、ユーザーの投稿及びその内容を、無償かつ無期限に複製、頒布、翻案、編集、展示、公衆送信、営業使用その他の方法により利用することができ、ユーザーはかかる利用を許諾するものとします。
当運営者及び当運営者が許諾した者は、ユーザーの投稿及びその内容を、理由の如何を問わず削除できるものとします。
ユーザーは、前2項について、著作者人格権を行使せず、異議を申し立てないものとします。
当運営者及び当運営者が許諾した者以外は、他のユーザーの投稿した内容について、複製、頒布、翻案、編集、展示、公衆送信、営業使用その他の方法による利用を行ってはならないものとします。
</p>
<h5>第7条(免責事項)</h5>
<p>
当運営者は、本サービスに事実上または法律上の瑕疵(安全性、信頼性、正確性、完全性、有効性、
特定の目的への適合性、セキュリティなどに関する欠陥、エラーやバグ、権利侵害などを含みます。)が
ないことを明示的にも黙示的にも保証しておりません。
当運営者は、本サービスに起因してユーザーに生じたあらゆる損害について一切の責任を負いません。
当運営者は、本サービスに関して、ユーザーと他のユーザーまたは第三者との間において生じた取引、 連絡または紛争等について一切責任を負いません。
</p>
<h5>第8条(サービス内容の変更等)</h5>
<p>
当運営者は、ユーザーに通知することなく、本サービスの内容を変更しまたは
本サービスの提供を中止若しくは停止することができるものとし、これによってユーザーに生じた損害について一切の責任を負いません。
</p>
<h5>第9条(利用規約の変更)</h5>
<p>
当運営者は、必要と判断した場合には、ユーザーに通知することなくいつでも本規約を 変更することができるものとします。なお、本規約の変更後、本サービスの利用を開始した場合には、
当該ユーザーは変更後の規約に同意したものとみなします。
</p>
<h5>第10条(個人情報の取扱い)</h5>
<p>
当運営者は、本サービスの利用によって取得する情報についてIPアドレスのみとなっております。
IPアドレスからは基本的に個人を特定することはできません。</p>
<h5>第11条(準拠法・裁判管轄)</h5>
<p>
本規約の解釈にあたっては、日本法を準拠法とします。
本サービスに関して紛争が生じた場合には、当運営者の普通裁判籍の所在地を管轄する裁判所を専属的合意管轄とします。
</p>
<p>本利用規約は2023年6月6日から施行します。
2023年6月6日 制定</p>
</div>
<h1 id="inquiry">お問い合わせ</h1>
<p>お問い合わせや要望、バグ・不具合報告は<a href="#">お問い合わせスレッド</a>か、<a href="#">開発者Twitter</a>までお願いします。</p>
</div>
</div>
<div class="col-2 right-col">
</div>
</div>
</div>
<footer>
<ul class="footer-nav">
<li><a href="/about">HerMitとは</a></li>
<li>|</li>
<li><a href="/about#terms">利用規約</a></li>
<li>|</li>
<li><a href="/about#inquiry">お問い合わせ</a></li>
</ul>
<p>© 2023 HerMit</p>
</footer>
<script th:src="@{/js/bootstrap.bundle.min.js}"></script>
</body>
</html>
CSS
hermitstyle1.css
@charset "UTF-8";
html {
font-size: 100%;
}
body {
font-family: sans-serif, "Ui Gothic Medium", "游ゴシック medium", YuGothic, "游ゴシック体", "ヒラギノ角ゴ Pro W3";
line-height: 1.7;
background-color: #c8edc3;
}
header {
background-color: #97cdf3;
display: flex;
justify-content: start;
max-width: 2000px;
margin: 0 auto;
padding: 0 4%;
border-bottom: 5px solid rgb(2, 57, 0);
}
.header-nav {
display: flex;
font-size: 1.5rem;
text-transform: uppercase;
margin-top: 40px;
list-style: none;
}
.header-nav li {
margin-left: 60px;
background-color: white;
}
.header-nav li:hover {
background-color: rgb(10, 31, 31);
}
.header-nav a {
text-decoration: none;
color: black;
}
.logo {
width: 210px;
margin-top: 10px;
}
.left-col {
border-right: 5px solid rgb(2, 57, 0);
background-color: #c8edc3;
}
.right-col {
border-left: 5px solid rgb(2, 57, 0);
background-color: #c8edc3;
}
.background-color-sentence {
color: #00011a;
}
.main-area {
text-align: center;
}
.main-area h2 {
margin-top: 25px;
margin-left: 10px;
margin-right: 10px;
margin-block-end: 25px;
background-color: #50a45b;
color: black;
border: 0.5rem rgb(2, 57, 0);
border-radius: 12px;
font: bold 1.3rem sans-serif;
box-shadow: 0 0 0 0.5rem #50a45b;
text-align: left;
}
.main-area h1 {
margin-top: 25px;
margin-left: 10px;
margin-right: 10px;
margin-block-end: 25px;
color: black;
text-align: left;
}
.main-area p,
.greenback {
margin-top: 25px;
margin-left: 10px;
margin-right: 10px;
margin-block-end: 25px;
background-color: #50a45b;
color: black;
border: 0.5rem rgb(2, 57, 0);
border-radius: 5px;
box-shadow: 0 0 0 0.5rem #50a45b;
text-align: left;
}
.main-area a {
text-decoration: none;
font-weight: normal;
}
.main-area h2:hover {
background-color: rgb(24, 44, 44);
box-shadow: 0 0 0 0.5rem rgb(24, 44, 44);
}
.main-area span {
font-size: 61%;
}
.form-thread-create {
margin-top: 30px;
}
.pageing span,
.pageing li,
.pageing a {
font: bold 1.3rem sans-serif;
margin: auto;
display: inline-block;
text-decoration: none;
}
.thread-create,
.thread-comment {
text-align: left;
}
input,
textarea {
background-color: rgb(24, 44, 44);
}
.create-buttton {
margin-bottom: 2rem;
}
button.btn--green {
color: #000000;
background-color: #094;
}
button.btn--green:hover {
color: white;
background: #00a349;
}
button.btn--green.btn--cubic {
border-bottom: 5px solid #00662d;
}
button.btn--green.btn--cubic:hover {
margin-top: 3px;
border-bottom: 2px solid #00662d;
}
button.btn-c {
font-size: 2rem;
position: relative;
border-radius: 100vh;
}
button.btn-c:before {
font-size: 1.6rem;
line-height: 1;
position: absolute;
top: calc(50% - .8rem);
right: 1rem;
margin: 0;
padding: 0;
}
footer {
background-color: #97cdf3;
justify-content: center;
max-width: 2000px;
margin: auto;
padding: 0 4%;
border-top: 5px solid rgb(2, 57, 0);
border-bottom: 5px solid rgb(2, 57, 0);
text-align: center;
}
.footer-nav {
display: flex;
font-size: 1rem;
text-transform: uppercase;
list-style: none;
text-align: center;
margin: auto;
justify-content: center;
}
.footer-nav li {
margin-left: 60px;
}
.footer-nav a {
text-decoration: none;
color: black;
}
.footer-nav a:hover {
color: rgb(10, 31, 31);
}
.validation {
display: inline-block;
color: red;
}
.jump {
display: inline-block;
margin-left: 80%;
margin-top: 1px;
text-align: right;
}
JavaScript
hermitscript.js
function check(){
if(window.confirm('送信してよろしいですか?')){ // 確認ダイアログを表示
return true; // 「OK」時は送信を実行
}
else{ // 「キャンセル」時の処理
window.alert('キャンセルされました'); // 警告ダイアログを表示
return false; // 送信を中止
}
}
その他
application.properties
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/HerMit
spring.datasource.username=postgres
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
management.add-application-context-header=false
server.port=80
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.0'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-security'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
備忘録
ドメイン取得してspringbootアプリをサーバー上でサービスとして実行するまで
conohavpsからドメインを買って、DNSでIP設定する。
反映されるまでは3時間もかからなかったような気がする。
参考:https://create-it-myself.com/know-how/set-domain-in-conoha-vps/
ConohaVPSでうぶんつサーバー追加してTeraTermで開いた
下記を実行
sudo apt update
sudo apt install openjdk-17-jre
sudo apt install openjdk-17-jdk
sudo apt install git
wget https://services.gradle.org/distributions/gradle-7.6.1-bin.zip
sudo apt-get install zip
sudo unzip -d /opt/gradle ./gradle-7.6.1-bin.zip
sudo vim /etc/profile.d/gradle.sh
作ったgradle.shに以下を記入
export GRADLE_HOME=/opt/gradle/gradle-7.6.1
export PATH=\${GRADLE_HOME}/bin:${PATH}
下記を実行
sudo chmod +x /etc/profile.d/gradle.sh
source /etc/profile.d/gradle.sh
ポート開放
sudo ufw allow 80
sudo ufw reload
sudo ufw status
DBインストール
sudo apt install postgresql-14
クエリでDB、テーブルを作成
ALTER USER user_name WITH PASSWORD 'password';
でパスワード作成
参考:https://qiita.com/ekzemplaro/items/0ceeb276826fd0e87b86
.jarを作ってたらそれをTeraTermでドラッグアンドドロップで持ってくることができるので、それやってたら下記は不要
sshキーを作成
ssh-keygen -t rsa -b 4096 -C "hoge@hoge"(hogeはメールアドレス)
.sshの中のid_rsa.pubの中身をgithubのアカウントにsshキー登録する。
githubからプログラムをコピーする
git clone git@github.com:hoge/hoge.git
gradle buildをやってbuildのlibsに実行可能.jarができてるので、それをサービスとしてシステムに登録する
sudo vim /etc/systemd/system/hoge.service
を作り下記を記入
[Unit]
Description = hoge daemon
[Service]
ExecStart = java -jar /hoge/hoge.jar
Restart = always
Type = simple
[Install]
WantedBy = multi-user.target
自動起動ONに
sudo systemctl enable hoge
プログラム起動
sudo systemctl start hoge
(プログラムのステータス確認コマンド)
sudo systemctl status hoge
詰まった部分は、サーバー上でプログラム動かそうとしてもデータベースがないので入れないといけなかった
入れても上手く動かず、パスワードを設定しないといけなかったらしい
あと、spring.jpa.hibernate.ddl-auto=updateとアプリケーションプロパティに記述しないと、シーケンス関係のせいでうまく動かなかった
さらに、サービスを作る時ExecStart = java -jar /hoge/hoge.jarで、「-」が抜けてたせいでうまく動かなかった
クエリ
DB作成
CREATE DATABASE hermit;
threadテーブル作成
create table thread(uuid text,creater text,title text,count integer,createdatetime timestamp without time zone,updatadatetime timestamp without time zone,firstcomment text);
commentsテーブル作成
create table comments(id serial PRIMARY KEY,threaduuid text,commentname text,comment text,createdatetime timestamp without time zone);
初期データ追加
insert into thread values ('otoiawase','名無し','お問い合わせスレッド',1,'2023-06-01 00:00:00.000','2023-06-01 00:00:00.000','お問い合わせはこちらまで');
insert into comments values(1,'otoiawase','名無し','お問い合わせはこちらまで','2023-06-01 00:00:00.000');
DB操作
ユーザ切り替え
su postgres
psql立ち上げ
psql
postgresとしてDBへ接続
\c hermit
テーブルデータ一覧取得
\d thread
参考サイト一覧
プログラミングの参考にしたサイトを適当に並べてるだけ
https://qiita.com/KenjiOtsuka/items/8450c407ba121fea8151
https://tokuty.hatenablog.com/entry/2021/03/14/Java_Vscode%E3%81%A7SpringBoot%E3%82%92jar%E3%81%AB%E3%81%99%E3%82%8B
https://www.javadrive.jp/postgresql/
https://qiita.com/oekazuma/items/4d8043760b6cdff2d401
https://qiita.com/yunity29/items/7ccc84d47e139340ecbc
https://qiita.com/kazuki43zoo/items/ce88dea403c596249e8a
https://qiita.com/malvageee/items/e6a26671a368af877c47
https://qiita.com/hatopo/items/64f802d845a481162b04
https://qiita.com/takahirocook/items/aa857c8f2153193095e4
https://ts0818.hatenablog.com/entry/2017/10/09/144626
https://qiita.com/tanibuchi12/items/6c8fedbc19bdb277d6f2#interface-paget
https://learning-collection.com/springboot%E5%85%A5%E9%96%80-vol-10%EF%BC%9A%E9%A1%A7%E5%AE%A2%E7%99%BB%E9%8C%B2%E7%94%BB%E9%9D%A2%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%82%88%E3%81%86/
https://qiita.com/TaikiTkwkbysh/items/ff005a5dcb63b3378cda#view
https://b1san-blog.com/post/spring/spring-jpa/
https://qiita.com/crml1206/items/bab47b15342e25343e3c
https://b1san-blog.com/post/spring/spring-lombok/#getter-setter
https://itsakura.com/db
https://spring.pleiades.io
https://www.sejuku.net/blog/61879
https://qiita.com/takaakitanaka_cre-co/items/1571bcd870bd64d83d15
https://qiita.com/fsd_itchy/items/384f05225e83f2705cf4
https://zenn.dev/angelica/books/52be1e365c61ea/viewer/f0683a
https://www.sejuku.net/blog/30245
https://qiita.com/hisamura333/items/e3ea6ae549eb09b7efb9
https://bootstrap-guide.com/example
https://www-creators.com/archives/241
https://qiita.com/fumikomatsu/items/3b539e79010a4808337c
参考書籍一覧
https://www.amazon.co.jp/gp/product/4297124904/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1
https://www.amazon.co.jp/gp/product/4297124297/ref=ppx_yo_dt_b_asin_title_o08_s00?ie=UTF8&psc=1
https://www.amazon.co.jp/dp/4815615756?psc=1&ref=ppx_yo2ov_dt_b_product_details
https://www.amazon.co.jp/gp/product/4797398892/ref=ppx_yo_dt_b_asin_title_o06_s00?ie=UTF8&psc=1
https://www.amazon.co.jp/dp/479806825X?psc=1&ref=ppx_yo2ov_dt_b_product_details
https://www.amazon.co.jp/gp/product/4295007803/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1
https://www.amazon.co.jp/dp/4798069167?psc=1&ref=ppx_yo2ov_dt_b_product_details
https://www.amazon.co.jp/gp/product/429501124X/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1