実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その49)
0. 初めに
こんにちは!
ゼロから始めるWebアプリケーション開発生活です。
前回まででコントローラーのFeatureテストが完了している状態でした。
今日は、Featureテストの残りを行っていきたいと思います。
1. ポリシー
まずは、Unitテスト編で後回しにしていたポリシーの一部をFeatureテストにて作成・実行します。
1.1 ブランチ運用
いつも通り、developブランチを最新化させて新規ブランチを切って作業をします。
ブランチ名は、test/feature/policiesとかでよいでしょう。
使用済みのブランチは削除しておきましょう。
1.2 レビューポリシー
実は、作るのはレビューポリシーだけです。
作成
実行コマンド
$ php artisan make:test Policies/ReviewPolicyTest
<?php
namespace Tests\Feature\Policies;
use App\Models\Lab;
use App\Models\Review;
use App\Models\User;
use App\Policies\ReviewPolicy;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ReviewPolicyTest extends TestCase
{
use RefreshDatabase;
public function test_レビューがない場合は作成できる(): void
{
// Arrange
$user = User::factory()->create();
$lab = Lab::factory()->create();
$policy = new ReviewPolicy();
// Act
$result = $policy->create($user, $lab);
// Assert
$this->assertTrue($result);
}
public function test_既にレビューがある場合は作成できない(): void
{
// Arrange
$user = User::factory()->create();
$lab = Lab::factory()->create();
Review::factory()->create([
'user_id' => $user->id,
'lab_id' => $lab->id,
]);
$policy = new ReviewPolicy();
// Act
$result = $policy->create($user, $lab);
// Assert
$this->assertFalse($result);
}
}
ファクトリーを用いて実際にDBへの操作を行うため、このcreateだけFeatureテストに回していました。
ファイル名が同じなので紛らわしいかもしれませんが、フォルダの場所は間違えないように気を付けましょう。
実行
実行コマンド
$ php artisan test --filter=ReviewPolicy
実行結果
同名のファイルで絞り込むとどうなるかと思って試してみたら、どっちも実行してくれているみたいです。
これで、UnitテストとFeatureテスト両方についてPASSしていることが分かります。
コミット・プッシュ、PR作成・マージ、ブランチ削除をお忘れなく!
2. モデル
2.1 ブランチ運用
いつも通り、developブランチを最新化させて新規ブランチを切って作業をします。
ブランチ名は、test/feature/modelsとかでよいでしょう。
2.2 研究室モデル
実は、これも作成するのは一つだけで、研究室モデルです。
作成
<?php
namespace Tests\Feature\Models;
use App\Models\Lab;
use App\Models\Review;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LabTest extends TestCase
{
use RefreshDatabase;
// ---------------------------------------------------------------
// scopeWithRatingAverages
// ---------------------------------------------------------------
public function test_レビューがない場合はavgがnullでreviewsCountが0(): void
{
$lab = Lab::factory()->create();
$result = Lab::withRatingAverages()->find($lab->id);
$this->assertNotNull($result);
$this->assertSame(0, (int) $result->reviews_count);
$this->assertNull($result->overall_avg);
foreach (Lab::RATING_COLUMNS as $column) {
$this->assertNull($result->{"avg_{$column}"}, "avg_{$column} should be null");
}
}
public function test_レビューが1件の場合は各avgがそのレビューの値と一致する(): void
{
$lab = Lab::factory()->create();
$ratings = [
'mentorship_style' => 3,
'lab_atmosphere' => 4,
'achievement_activity' => 5,
'constraint_level' => 2,
'facility_quality' => 1,
'work_style' => 3,
'student_balance' => 4,
];
Review::factory()->create(array_merge(['lab_id' => $lab->id], $ratings));
$result = Lab::withRatingAverages()->find($lab->id);
$this->assertSame(1, (int) $result->reviews_count);
foreach (Lab::RATING_COLUMNS as $column) {
$this->assertEquals(
round($ratings[$column], 3),
(float) $result->{"avg_{$column}"},
"avg_{$column} mismatch"
);
}
$expectedOverall = round(array_sum($ratings) / count($ratings), 3);
$this->assertEquals($expectedOverall, (float) $result->overall_avg);
}
public function test_レビューが複数件の場合はavgが正しく計算される(): void
{
$lab = Lab::factory()->create();
Review::factory()->create(array_merge(
['lab_id' => $lab->id],
array_fill_keys(Lab::RATING_COLUMNS, 2)
));
Review::factory()->create(array_merge(
['lab_id' => $lab->id],
array_fill_keys(Lab::RATING_COLUMNS, 4)
));
$result = Lab::withRatingAverages()->find($lab->id);
$this->assertSame(2, (int) $result->reviews_count);
foreach (Lab::RATING_COLUMNS as $column) {
$this->assertEquals(3.0, (float) $result->{"avg_{$column}"}, "avg_{$column} mismatch");
}
$this->assertEquals(3.0, (float) $result->overall_avg);
}
public function test_他のLabのレビューは集計に含まれない(): void
{
$lab1 = Lab::factory()->create();
$lab2 = Lab::factory()->create();
Review::factory()->create(array_merge(
['lab_id' => $lab2->id],
array_fill_keys(Lab::RATING_COLUMNS, 5)
));
$result = Lab::withRatingAverages()->find($lab1->id);
$this->assertSame(0, (int) $result->reviews_count);
$this->assertNull($result->overall_avg);
}
public function test_スコープは複数Labを一括取得できる(): void
{
$lab1 = Lab::factory()->create();
$lab2 = Lab::factory()->create();
Review::factory()->create(array_merge(
['lab_id' => $lab1->id],
array_fill_keys(Lab::RATING_COLUMNS, 5)
));
// lab2 はレビューなし
$results = Lab::withRatingAverages()
->whereIn('labs.id', [$lab1->id, $lab2->id])
->get();
$this->assertCount(2, $results);
$r1 = $results->firstWhere('id', $lab1->id);
$r2 = $results->firstWhere('id', $lab2->id);
$this->assertSame(1, (int) $r1->reviews_count);
$this->assertEquals(5.0, (float) $r1->overall_avg);
$this->assertSame(0, (int) $r2->reviews_count);
$this->assertNull($r2->overall_avg);
}
}
初めて見るPHPの関数もあるかもしれませんが、Googleで検索してもらえればどれもすぐに使い方が分かるものばかりだと思います。
特にPHPは、公式のドキュメントが充実していて日本語で関数の使い方の具体例まで載せてくれていてとても分かりやすいのでお勧めです。
実行
実行コマンド
$ php artisan test --filter=LabTest
Unitテストと合わせて合格です!
コミット・プッシュ、PR作成・マージ、ブランチ削除を忘れずに行いましょう。
3. おまけ(Unitテスト漏れ対応)
ここからは、本当はUnitテストで対応するべきだったけど抜け漏れていた部分を対応していきたいと思います。(笑)
3.1 ブランチ運用
例によって、developブランチを最新化させて、新規にtest/unit/remainingみたいな名前のブランチを切って作業をします。
3.2 ブックマークポリシー
作成
createメソッドについてのテストケースが抜け漏れていました。
DB操作を伴わないので、Unitテストで対応するべきでした。
既存ファイルに二つテストケースを追加してください。
class BookmarkPolicyTest extends TestCase
{
public function test_ログイン済みユーザーはブックマークを作成できる(): void
{
$user = new User();
$user->exists = true;
$policy = new BookmarkPolicy();
$this->assertTrue($policy->create($user));
}
public function test_未ログインユーザーはブックマークを作成できない(): void
{
$user = new User();
$user->exists = false;
$policy = new BookmarkPolicy();
$this->assertFalse($policy->create($user));
}
// 残りのテストケース...
実行
実行コマンド
$ php artisan test --filter=BookMarkPolicyTest
3.3 コメントポリシー
作成
ブックマークポリシーと同様です。
class CommentPolicyTest extends TestCase
{
public function test_ログイン済みユーザーはコメントを作成できる(): void
{
$user = new User();
$user->exists = true;
$policy = new CommentPolicy();
$this->assertTrue($policy->create($user));
}
public function test_未ログインユーザーはコメントを作成できない(): void
{
$user = new User();
$user->exists = false;
$policy = new CommentPolicy();
$this->assertFalse($policy->create($user));
}
// 既存の他のテストケース...
実行
実行コマンド
$ php artisan test --filter=CommentPolicyTest
3.4 大学ポリシー
作成
createメソッドだけでなく、updateも抜けていたので、追加してください。
class UniversityPolicyTest extends TestCase
{
public function test_ログイン済みユーザーは大学を作成できる(): void
{
$user = new User();
$user->exists = true;
$policy = new UniversityPolicy();
$this->assertTrue($policy->create($user));
}
public function test_未ログインユーザーは大学を作成できない(): void
{
$user = new User();
$user->exists = false;
$policy = new UniversityPolicy();
$this->assertFalse($policy->create($user));
}
public function test_ログイン済みユーザーは大学を更新できる(): void
{
$user = new User();
$user->exists = true;
$policy = new UniversityPolicy();
$this->assertTrue($policy->update($user));
}
public function test_未ログインユーザーは大学を更新できない(): void
{
$user = new User();
$user->exists = false;
$policy = new UniversityPolicy();
$this->assertFalse($policy->update($user));
}
// 残りのテストケース...
実行
実行コマンド
$ php artisan test --filter=UniversityPolicyTest
3.5 学部ポリシー
作成
同様です。
class FacultyPolicyTest extends TestCase
{
public function test_ログイン済みユーザーは学部を作成できる(): void
{
$user = new User();
$user->exists = true;
$policy = new FacultyPolicy();
$this->assertTrue($policy->create($user));
}
public function test_未ログインユーザーは学部を作成できない(): void
{
$user = new User();
$user->exists = false;
$policy = new FacultyPolicy();
$this->assertFalse($policy->create($user));
}
public function test_ログイン済みユーザーは学部を更新できる(): void
{
$user = new User();
$user->exists = true;
$policy = new FacultyPolicy();
$this->assertTrue($policy->update($user));
}
public function test_未ログインユーザーは学部を更新できない(): void
{
$user = new User();
$user->exists = false;
$policy = new FacultyPolicy();
$this->assertFalse($policy->update($user));
}
// その他のテストケース...
実行
実行コマンド
$ php artisan test --filter=FacultyPolicyTest
3.6 研究室ポリシー
作成
こちらも同様です。
class LabPolicyTest extends TestCase
{
public function test_ログイン済みユーザーは研究室を作成できる(): void
{
$user = new User();
$user->exists = true;
$policy = new LabPolicy();
$this->assertTrue($policy->create($user));
}
public function test_未ログインユーザーは研究室を作成できない(): void
{
$user = new User();
$user->exists = false;
$policy = new LabPolicy();
$this->assertFalse($policy->create($user));
}
public function test_ログイン済みユーザーは研究室を更新できる(): void
{
$user = new User();
$user->exists = true;
$policy = new LabPolicy();
$this->assertTrue($policy->update($user));
}
public function test_未ログインユーザーは研究室を更新できない(): void
{
$user = new User();
$user->exists = false;
$policy = new LabPolicy();
$this->assertFalse($policy->update($user));
}
実行
実行コマンド
$ php artisan test --filter=LabPolicyTest
大変失礼いたしました!
全てPASSすれば、コミット・プッシュ、PR作成・マージ・ブランチ削除をお忘れなく!
4. おまけその2(Featureテスト漏れ対応)
次に、Featureテストの漏れを対応していきましょう。
4.1 ブランチ運用
developブランチを最新化させて、新規にtest/feature/remainingというブランチを切って作業をしましょう。
先ほど使った、 test/unit/remainingブランチはもう使わないので削除しておきましょう。
4.2 大学コントローラー
updateメソッドには、更新処理の副作用として自分以外のユーザーが作成したものの場合、通知を送信するという処理がありました。
この通知が送信されれるかどうかという観点が抜けていたので、追加したいと思います。
作成
既存のファイルに追記しましょう。
<?php
namespace Tests\Feature\Controllers;
use App\Models\University;
use App\Models\User;
use App\Notifications\ModelChangedNotification; // 追加
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification; // 追加
use Tests\TestCase;
class UniversityControllerTest extends TestCase
{
use RefreshDatabase;
// 既存メソッド...
// updateの下に追記
public function test_他のユーザーが大学を更新すると作成者へ通知が送られる(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$updater = User::factory()->create();
$university = University::factory()->create([
'created_by' => $creator->id,
'name' => '更新前大学',
'type' => 'national',
'version' => 1,
]);
// Act
$this->actingAs($updater)
->put(route('universities.update', $university), [
'name' => '更新後大学',
'type' => 'public',
'comment' => '名称変更',
'version' => 1,
]);
// Assert
Notification::assertSentTo($creator, ModelChangedNotification::class);
}
public function test_作成者自身が大学を更新しても通知は送られない(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$university = University::factory()->create([
'created_by' => $creator->id,
'name' => '更新前大学',
'type' => 'national',
'version' => 1,
]);
// Act
$this->actingAs($creator)
->put(route('universities.update', $university), [
'name' => '更新後大学',
'type' => 'public',
'comment' => '自分で更新',
'version' => 1,
]);
// Assert
Notification::assertNothingSent();
}
// history...
実行
実行コマンド
$ php artisan test --filter=UniversityControllertest
4.3 学部コントローラー
同様です。
作成
<?php
namespace Tests\Feature\Controllers;
use App\Models\Faculty;
use App\Models\University;
use App\Models\User;
use App\Notifications\ModelChangedNotification; // 追加
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification; // 追加
use Tests\TestCase;
class FacultyControllerTest extends TestCase
{
use RefreshDatabase;
// 既存メソッド...
// updateの下に追記
public function test_他のユーザーが学部を更新すると作成者へ通知が送られる(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$updater = User::factory()->create();
$faculty = Faculty::factory()->create([
'created_by' => $creator->id,
'name' => '更新前学部',
'version' => 1,
]);
// Act
$this->actingAs($updater)
->put(route('faculties.update', $faculty), [
'name' => '更新後学部',
'comment' => '名称変更',
'version' => 1,
]);
// Assert
Notification::assertSentTo($creator, ModelChangedNotification::class);
}
public function test_作成者自身が学部を更新しても通知は送られない(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$faculty = Faculty::factory()->create([
'created_by' => $creator->id,
'name' => '更新前学部',
'version' => 1,
]);
// Act
$this->actingAs($creator)
->put(route('faculties.update', $faculty), [
'name' => '更新後学部',
'comment' => '自分で更新',
'version' => 1,
]);
// Assert
Notification::assertNothingSent();
}
実行コマンド
$ php artisan test --filter=FacultyControllertest
4.4 研究室コントローラー
同様です。
作成
<?php
namespace Tests\Feature\Controllers;
use App\Models\Faculty;
use App\Models\Lab;
use App\Models\Review;
use App\Models\User;
use App\Notifications\ModelChangedNotification; // 追加
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification; // 追加
use Tests\TestCase;
class LabControllerTest extends TestCase
{
use RefreshDatabase;
// 既存メソッド,...
// updateの下に追記
public function test_他のユーザーが研究室を更新すると作成者へ通知が送られる(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$updater = User::factory()->create();
$lab = Lab::factory()->create([
'created_by' => $creator->id,
'name' => '更新前研究室',
'version' => 1,
]);
// Act
$this->actingAs($updater)
->put(route('labs.update', $lab), [
'name' => '更新後研究室',
'description' => null,
'url' => null,
'professor_name' => null,
'professor_url' => null,
'gender_ratio_male' => 6,
'gender_ratio_female' => 4,
'comment' => '名称変更',
'version' => 1,
]);
// Assert
Notification::assertSentTo($creator, ModelChangedNotification::class);
}
public function test_作成者自身が研究室を更新しても通知は送られない(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$lab = Lab::factory()->create([
'created_by' => $creator->id,
'name' => '更新前研究室',
'version' => 1,
]);
// Act
$this->actingAs($creator)
->put(route('labs.update', $lab), [
'name' => '更新後研究室',
'description' => null,
'url' => null,
'professor_name' => null,
'professor_url' => null,
'gender_ratio_male' => 6,
'gender_ratio_female' => 4,
'comment' => '自分で更新',
'version' => 1,
]);
// Assert
Notification::assertNothingSent();
}
実行
実行コマンド
$ php artisan test --filter=LabControllertest
4.5 管理者コントローラー
destroy〇〇メソッドで通知送信が行われるので同様に追加です。
<?php
namespace Tests\Feature\Controllers\Admin;
use App\Models\Comment;
use App\Models\DeletionRequest; // 追加
use App\Models\Faculty;
use App\Models\Lab;
use App\Models\University;
use App\Models\User;
use App\Notifications\DeletionCompletedNotification; // 追加
use App\Notifications\ModelChangedNotification; // 追加
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification; // 追加
use Tests\TestCase;
class AdminControllerTest extends TestCase
{
use RefreshDatabase;
// destroyUniversityの下に追記
public function test_大学削除時に削除依頼者へDeletionCompletedNotificationが送られる(): void
{
// Arrange
Notification::fake();
$admin = User::factory()->create(['is_admin' => true]);
$requester = User::factory()->create();
$university = University::factory()->create();
DeletionRequest::create([
'requested_by' => $requester->id,
'target_type' => University::class,
'target_id' => $university->id,
'status' => 'pending',
]);
// Act
$this->actingAs($admin)
->delete(route('admin.universities.destroy', $university));
// Assert
Notification::assertSentTo($requester, DeletionCompletedNotification::class);
}
public function test_大学削除時に作成者へModelChangedNotificationが送られる(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$admin = User::factory()->create(['is_admin' => true]);
$university = University::factory()->create(['created_by' => $creator->id]);
// Act
$this->actingAs($admin)
->delete(route('admin.universities.destroy', $university));
// Assert
Notification::assertSentTo($creator, ModelChangedNotification::class);
}
public function test_管理者自身が作成した大学を削除しても作成者へ通知は送られない(): void
{
// Arrange
Notification::fake();
$admin = User::factory()->create(['is_admin' => true]);
$university = University::factory()->create(['created_by' => $admin->id]);
// Act
$this->actingAs($admin)
->delete(route('admin.universities.destroy', $university));
// Assert
Notification::assertNothingSent();
}
// destroyFacultyの下に追記
public function test_学部削除時に削除依頼者へDeletionCompletedNotificationが送られる(): void
{
// Arrange
Notification::fake();
$admin = User::factory()->create(['is_admin' => true]);
$requester = User::factory()->create();
$faculty = Faculty::factory()->create();
DeletionRequest::create([
'requested_by' => $requester->id,
'target_type' => Faculty::class,
'target_id' => $faculty->id,
'status' => 'pending',
]);
// Act
$this->actingAs($admin)
->delete(route('admin.faculties.destroy', $faculty));
// Assert
Notification::assertSentTo($requester, DeletionCompletedNotification::class);
}
public function test_学部削除時に作成者へModelChangedNotificationが送られる(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$admin = User::factory()->create(['is_admin' => true]);
$faculty = Faculty::factory()->create(['created_by' => $creator->id]);
// Act
$this->actingAs($admin)
->delete(route('admin.faculties.destroy', $faculty));
// Assert
Notification::assertSentTo($creator, ModelChangedNotification::class);
}
public function test_管理者自身が作成した学部を削除しても作成者へ通知は送られない(): void
{
// Arrange
Notification::fake();
$admin = User::factory()->create(['is_admin' => true]);
$faculty = Faculty::factory()->create(['created_by' => $admin->id]);
// Act
$this->actingAs($admin)
->delete(route('admin.faculties.destroy', $faculty));
// Assert
Notification::assertNothingSent();
}
// destroyLabの下に追記
public function test_研究室削除時に削除依頼者へDeletionCompletedNotificationが送られる(): void
{
// Arrange
Notification::fake();
$admin = User::factory()->create(['is_admin' => true]);
$requester = User::factory()->create();
$lab = Lab::factory()->create();
DeletionRequest::create([
'requested_by' => $requester->id,
'target_type' => Lab::class,
'target_id' => $lab->id,
'status' => 'pending',
]);
// Act
$this->actingAs($admin)
->delete(route('admin.labs.destroy', $lab));
// Assert
Notification::assertSentTo($requester, DeletionCompletedNotification::class);
}
public function test_研究室削除時に作成者へModelChangedNotificationが送られる(): void
{
// Arrange
Notification::fake();
$creator = User::factory()->create();
$admin = User::factory()->create(['is_admin' => true]);
$lab = Lab::factory()->create(['created_by' => $creator->id]);
// Act
$this->actingAs($admin)
->delete(route('admin.labs.destroy', $lab));
// Assert
Notification::assertSentTo($creator, ModelChangedNotification::class);
}
public function test_管理者自身が作成した研究室を削除しても作成者へ通知は送られない(): void
{
// Arrange
Notification::fake();
$admin = User::factory()->create(['is_admin' => true]);
$lab = Lab::factory()->create(['created_by' => $admin->id]);
// Act
$this->actingAs($admin)
->delete(route('admin.labs.destroy', $lab));
// Assert
Notification::assertNothingSent();
}
作成
実行
実行コマンド
$ php artisan test --filter=AdminControllertest
コミット・プッシュ、PR作成・マージ、ブランチ削除を忘れずに行いましょう!
5. まとめ・次回予告
お疲れ様です。
本日をもって、Featureテストは完了です!
次回からは、E2Eテストに取り掛かりたいと思います。
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
☆バックエンド実装編
- その7: バックエンド実装編① ~認証機能作成~
- その8: バックエンド実装編②前編 ~レビュー投稿機能作成~
- その8.5: バックエンド実装編②後編 ~レビュー投稿機能作成~
- その9: バックエンド実装編③ ~レビューCRUD機能作成~
- その10: バックエンド実装編④ ~レビューCRUD機能作成その2~
- その11: バックエンド実装編⑤ ~新規大学・学部・研究室作成機能作成~
- その12: バックエンド実装編⑥ ~大学検索機能作成~
- その13: バックエンド実装編⑦ ~大学・学部・研究室編集機能作成~
- その14: バックエンド実装編⑧ ~コメント投稿機能~
- その15: バックエンド実装編⑨ ~コメント編集・削除機能~
- その16: バックエンド実装編⑩ ~ブックマーク機能~
- その17: バックエンド実装編⑪ ~排他制御・トランザクション処理~
- その18: バックエンド実装編⑫ ~マイページ機能作成~
- その19: バックエンド実装編⑬ ~管理者アカウント機能作成~
- その20: バックエンド実装編⑭ ~通知機能作成~
- その21: バックエンド実装編⑮ ~ソーシャルログイン機能作成~
☆フロントエンド実装編
- その22: フロントエンド実装編① ~メインコンテンツ領域作成~
- その23: フロントエンド実装編② ~ヘッダー作成~
- その24: フロントエンド実装編③ ~サイドバー作成~
- その25: フロントエンド実装編④ ~ホームページ作成~
- その26: フロントエンド実装編⑤ ~大学検索結果画面作成~
- その27: フロントエンド実装編⑥ ~大学詳細・学部一覧画面作成~
- その28: フロントエンド実装編⑦ ~学部詳細・研究室一覧画面作成~
- その29: フロントエンド実装編⑧前編 ~研究室詳細・レビュー画面作成前編~
- その29.5: フロントエンド実装編⑧後編 ~研究室詳細・レビュー画面作成後編~
- その30: フロントエンド実装編⑨ ~マイページ作成~
- その31: フロントエンド実装編⑩ ~パンくずリスト作成~
- その32: フロントエンド実装編⑪ ~ログイン・新規登録モーダル作成~
- その33: フロントエンド実装編⑫ ~大学作成・編集モーダル作成~
- その34: フロントエンド実装編⑬ ~学部作成・編集モーダル作成~
- その35: フロントエンド実装編⑭ ~研究室作成・編集モーダル作成~
- その36: フロントエンド実装編⑮ ~レビュー作成・編集モーダル作成~
- その37: フロントエンド実装編⑯ ~コメント一覧表示モーダル作成~
- その38: フロントエンド実装編⑰ ~編集履歴ページ・削除依頼ページ・通知ドロップダウン作成~
- その39: フロントエンド実装編⑱ ~トースト作成~
- その40: フロントエンド実装編⑲ ~退会ページ作成~
- その41: フロントエンド実装編⑳ ~ファビコン作成~
- その42: フロントエンド実装編㉑ ~レスポンシブデザイン対応~










