htmxで要素がビューポートにスクロールされた場合にリクエストを発行する方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、htmxで要素がビューポートにスクロールされた場合にAJAXリクエストを発行する方法をご紹介します。

実現したいこと

冒頭、実現したいことを整理します。

htmxではHTMLタグに専用の属性を記述することで、AJAXリクエストを発行できます。

htmx.org

<!-- クリックするとPOSTリクエストが発行される -->
<button hx-post="/path/to/dir" hx-trigger="click">...</button>

お示しの例では、hx-trigger="click"により、要素のクリックイベントをトリガーとしてリクエストが発行されます。

一方で今回実現したいのは、要素がビューポートにスクロールされた(表示領域に要素が到達した)ことをトリガーにリクエストを発行することです。

hx-targetの設定で実現可能

結論から申し上げると、AJAXリクエストのトリガーを指定する hx-trigger属性に revealed を設定することで実現可能です。

<!-- 要素がビューポートにスクロールされるとPOSTリクエストが発行される -->
<div hx-post="/path/to/dir" hx-trigger="revealed">...</div>

検証してみる

以下のサンプルで検証してみます。

<div hx-post="/path/to/dir" hx-trigger="revealed" hx-confirm="POSTしますか?">...</div>
  • hx-postでPOSTリクエストを指定
  • hx-trigger="revealed"で要素がビューポートにスクロールされたことをトリガーに指定
  • 実行確認のためにhx-confirmwindow.confirmを実行

hx-postを指定した要素がビューポートにスクロールされるとwindow.confirmが実行される

overflowプロパティが適用された要素内で使用する場合は注意

要素内スクロールなど、CSSoverflowプロパティが適用された要素の中では hx-trigger="revealed"が期待通りに動作しません。

見かけ上は表示されていなくても、ビューポート内と判定されてしまいページ読み込み直後にリクエストが発行されてしまいます。

その場合は、revealedの代わりに intersect onceと設定する必要があります。

まとめ

今回は、htmxで要素がビューポートにスクロールされた場合にAJAXリクエストを発行する方法をご紹介しました。

hx-trigger="revealed"により、無限スクロールなどのユーザー体験を簡単かつシンプルに実装することが可能です。ぜひお試しください。

htmxを使用される方の一助となれば幸いです。ご精読ありがとうございました。

参考文献

SpringBoot x MyBatis でDBのReader/Writerの接続設定

エキサイト株式会社エンジニアの佐々木です。SpringBoot x MyBatis でDBのReader/Writerの接続設定をご紹介します。

application.ymlの設定

application.ymlを下記のように

spring:
  datasource:
    writer:
      username: aaa
      password: bbb
      hikari:
        max-lifetime: 600000
        maximum-pool-size: 5
        connection-test-query: "SELECT 1;"
        keep-alive-time: 60000
    reader:
      username: aaa
      password: bbb
      hikari:
        max-lifetime: 600000
        maximum-pool-size: 10
        connection-test-query: "SELECT 1;"
        keep-alive-time: 60000

Reader/Writerの設定

Reader/Writerの設定は下記のようにコードを書きます。他DBに接続するときにも使えるの汎用的なコードになります。@Beanに名前をつけ、@Qualifyで取り出しながらDataSourceの設定を行います。


// DataSourceWriterConfig.java 

@Configuration
public class DataSourceWriterConfig {

    @Bean("hikariWriter")
    @ConfigurationProperties(prefix = "spring.datasource.writer.hikari") // spring.datasource.writer.hikariのHikariCP読み込みを設定
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }

    @Bean(name = "dbWriterProperties")
    @ConfigurationProperties(prefix = "spring.datasource.writer") // spring.datasource.writer の読み込み設定
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "dbWriter")
    @DependsOn("dbWriterProperties")
    public DataSource dataSource(
            @Qualifier("hikariWriter") HikariConfig hikariConfig,
            @Qualifier("dbWriterProperties") DataSourceProperties dataSourceProperties) {
        hikariConfig.setDataSource(
                dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build()
        );
        return new HikariDataSource(hikariConfig);
    }
}


// DataSourceReaderConfig.java 

@Configuration
public class DataSourceReaderConfig {

    @Bean("hikariReader")
    @ConfigurationProperties(prefix = "spring.datasource.reader.hikari") // spring.datasource.reader.hikariのHikariCP読み込みを設定
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }

    @Bean(name = "dbReaderProperties")
    @ConfigurationProperties(prefix = "spring.datasource.reader") // spring.datasource.reader の読み込みを設定
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "dbReader")
    @DependsOn("dbReaderProperties")
    public DataSource dataSource(
            @Qualifier("hikariReader") HikariConfig hikariConfig,
            @Qualifier("dbReaderProperties") DataSourceProperties dataSourceProperties) {
        hikariConfig.setDataSource(
                dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build()
        );
        return new HikariDataSource(hikariConfig);
    }

トランザクションまわりの全体コード

下記がDBの接続設定を処理するときのトランザクションの設定になります。複数DBに接続する必要があるときは、この設定が増えていきます。(参照用、書き込み用、集計用など...)

// DataSourceTransactionConfig.java

@Configuration
@RequiredArgsConstructor
@MapperScan(basePackages = "jp.co.excite.sample.sql")
public class DataSourceTransactionConfig {
    
    private final MybatisAutoConfiguration mybatisAutoConfiguration;

    @Bean("replicationResolver")
    public ReplicationResolver routingDataSource(@Qualifier(DataSourceWriterConfig.DATASOURCE_NAME) DataSource dataSourceWriter,
                                                 @Qualifier(DataSourceReaderConfig.DATASOURCE_NAME) DataSource dataSourceReader) {

        ReplicationResolver replicationDataSource = new ReplicationResolver();
        replicationDataSource.setTargetDataSources(Map.of(DataSourceType.READER, dataSourceReader, DataSourceType.WRITER, dataSourceWriter));
        replicationDataSource.setDefaultTargetDataSource(dataSourceReader);
        return replicationDataSource;
    }

    @Bean("mainTransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier("readerDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean("mainDataSource")
    @Primary
    public DataSource dataSource(@Qualifier("replicationResolver") ReplicationResolver dataSource) {
        return new LazyConnectionDataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("mainDataSource") DataSource dataSource) throws Exception {
        return mybatisAutoConfiguration.sqlSessionFactory(dataSource);
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    static class ReplicationResolver extends AbstractRoutingDataSource {

        @Override
        protected DataSourceType determineCurrentLookupKey() {
            return TransactionSynchronizationManager
                    .isCurrentTransactionReadOnly() ? DataSourceType.READER : DataSourceType.WRITER;
        }
    }

    static enum DataSourceType {
        WRITER,
        READER
    }
}

一つ一つずつみていきます。

ReplicationResolver, DataSourceType

下記コードをから、トランザクションの設定を行います。

    static class ReplicationResolver extends AbstractRoutingDataSource {

        @Override
        protected DataSourceType determineCurrentLookupKey() {
            return TransactionSynchronizationManager
                    .isCurrentTransactionReadOnly() ? DataSourceType.READER : DataSourceType.WRITER;
        }
    }

    static enum DataSourceType {
        WRITER,
        READER
    }

やっていることは簡単で、DataSourceType は、Enumを利用し、書き込みと読み込みを区別する目印を設定します。ReplicationResolverは、AbstractRoutingDataSourceを継承して、Routingの条件設定などを行います。シャードキーやリードオンリーのようなせていが入っているので、シンプルな要件だと実装は簡単だと思います。今回は、isCurrentTransactionReadOnly() のときに、参照用DBか更新用DBかの判定いれるために上記のように設定します。

ReplicationResolverのBean化

先ほど宣言したReplicationResolverを、Bean化します。今回は、ReaderDBとWriterDBに振り分けますので、その設定も行います。

    @Bean("replicationResolver")
    public ReplicationResolver routingDataSource(@Qualifier("dbWriter") DataSource dataSourceWriter,
                                                 @Qualifier("dbReader") DataSource dataSourceReader) {

        ReplicationResolver replicationDataSource = new ReplicationResolver();
        replicationDataSource.setTargetDataSources(Map.of(
                    DataSourceType.READER, dataSourceReader, 
                    DataSourceType.WRITER, dataSourceWriter
        ));
        replicationDataSource.setDefaultTargetDataSource(dataSourceReader);
        return replicationDataSource;
    }

setTargetDataSources()に、振り分けたいデータソースを設定します。デフォルトのデータソースも設定をします。

DataSource

Reader/WriterのときにもDataSourceは宣言したが、これはトランザクション用のDataSourceになります。ReplicationResolverでセットしたものから生成します。@Primaryをつけているのは、複数のDataSource定義があると起動時にエラーになりますので、デフォルトで接続する先を指定するためにトランザクション設定がされているものに@Primaryをつけておきます。

    @Bean("mainDataSource")
    @Primary
    public DataSource dataSource(@Qualifier("replicationResolver") ReplicationResolver replicationResolver) {
        return new LazyConnectionDataSourceProxy(replicationResolver);
    }

PlatformTransactionManager

トランザクション管理するAPIを提供するインターフェースで、今回はMyBatisを使うので、DataSourceTransactionManagerクラスを使用します。今回は1つしか設定しませんので、@Primaryは不要ですが今後のことを考えてつけておきます。

    @Bean("mainTransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(DATASOURCE_MAIN) DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

複数のTransactionManagerの設定がある場合は、利用時に@Transactionalの引数で@Transactional( transactionManager = "mainTransactionManager")のような形で明示的に呼び出しが可能です。指定がない場合は、@Primaryが設定されているTransactionManagerが使用されます。

SqlSessionFactory

application.ymlからMyBatisの設定を読み込み、そこからSqlSessionFactoryDataSourceをつなぎ設定を行います。

    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("mainDataSource") DataSource dataSource) throws Exception {
        return mybatisAutoConfiguration.sqlSessionFactory(dataSource);
    }

SqlSessionTemplate

上記で作成した、SqlSessionFactoryをSqlSessionTemplateのコンストラクタに渡しBean化します。

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

まとめ

DataSourceが合計3つでてくるので、最初は混乱すると思いますが、Bean名をつけて区別しながら設定すればうまくいくと思います。AOPで切り替える方法もネットには転がっていますが、ブラックボックスになりがちなので、公式に提供されているものをなるべく駆使しております。

ご参考になればと思います。

[Spring Boot] @ModelAttributeの使い方 [Java]

はじめに

こんにちは、新卒2年目の岡崎です。今回は@ModelAttributeの使い方を紹介します。

環境

  • Gradleのバージョン
------------------------------------------------------------
Gradle 8.11.1
------------------------------------------------------------

Build time:    2024-11-20 16:56:46 UTC
Revision:      481cb05a490e0ef9f8620f7873b83bd8a72e7c39

Kotlin:        2.0.20
Groovy:        3.0.22
Ant:           Apache Ant(TM) version 1.10.14 compiled on August 16 2023
Launcher JVM:  21.0.5 (Amazon.com Inc. 21.0.5+11-LTS)
OS:            Mac OS X 14.7.2 aarch64
  • Javaのバージョン
openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing)
  • Spring Bootのバージョン
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.0)

準備

gradleに以下の依存関係を追加してください。

 implementation 'org.springframework.boot:spring-boot-starter-web'

@ModelAttributeについて

@ModelAttributeは、リクエストパラメータをオブジェクトに変換し、Controllerの引数として受け取れるアノテーションです。

@ModelAttributeを引数につける

@ModelAttributeを引数につけると、リクエストパラメータが自動的にオブジェクトにマッピングされます。

例えば、userIduserNameをリクエストパラメータとして受け取り、画面に表示するエンドポイントを作成してみます。

@RestController
public class TestController {
    private final static String TEST_FORMAT = "userId: {userId}, userName: {userName}";

    @GetMapping("test/user")
    public String getTest(
            @ModelAttribute GetUserRequestModel requestModel
    ) {
        return TEST_FORMAT
                .replace("{userId}", requestModel.getUserId())
                .replace("{userName}", requestModel.getUserName());
    }

    @Data
    private static class GetUserRequestModel {
        private String userId;

        private String userName;
    }
}

http://localhost:ポート番号/test/user?userId=123&userName=太郎にアクセスすると、userIduserNameが画面に表示されました。

@ModelAttributeを使うことで、複数のリクエストパラメータをまとめてオブジェクトとして扱えます。これにより、コードの可読性が上がることが期待できます。

@ModelAttributeをメソッドにつける

@ModelAttributeは、メソッドにつけることもできます。 @ModelAttributeをメソッドにつけると、Controllerにあるエンドポイントが実行される前に、そのメソッドの処理が適用されます。

例えば、userNameadminでない場合にエラーを返すようなバリデーションを実装します。

@RestController
public class SampleController {
    @ModelAttribute
    public void Valid(String userName) {
        if (StringUtils.isBlank(userName)) {
            throw new BadRequestException("userNameが存在しません");
        }
    }

    @GetMapping("userName/exist")
    public String getUserName() {
        return "user name is exist.";
    }
}

http://localhost:ポート番号/userName/existにアクセスすると、userNameがないため、Bad Requestになりました。

BadRequestException: userNameが存在しません

http://localhost:ポート番号/userName/exist?userName=sampleにアクセスすると、以下の画面が表示されました。

このように、@ModelAttributeをメソッドの前につけることにより、バリデーション処理などを共通化し、コードを簡潔に整理できます。

最後に

今回は@ModelAttributeの使い方を紹介しました。誰かの参考になれば幸いです。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひ連絡よろしくお願いいたします。

www.wantedly.com

Tailwind CSSで子孫要素にアクセスする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回はTailwind CSSで子孫要素にアクセスする方法をご紹介します。

なぜ子孫要素にアクセスしたいか

冒頭、なぜ子孫要素にアクセスしたいか、その動機について整理します。

例えば、以下のようなUIをTailwind CSSを用いてスタイリングするとします。

スタイリングしたいUIの例

この場合、HTMLの構造は次の通りになります。

<div>
  <img alt="..." src="..." />
  <img alt="..." src="..." />
  <img alt="..." src="..." />
  <img alt="..." src="..." />
  <img alt="..." src="..." />
</div>

Tailwind CSSは個別の要素に対してユーティリティクラスを付与することでスタイリングするため、同じスタイルを5つのimg要素に対して繰り返す必要があります。

<div>
  <img class="w-8 aspect-square rounded-full ring-4 ring-white" alt="..." src="..." />
  <img class="w-8 aspect-square rounded-full ring-4 ring-white" alt="..." src="..." />
  <img class="w-8 aspect-square rounded-full ring-4 ring-white" alt="..." src="..." />
  <img class="w-8 aspect-square rounded-full ring-4 ring-white" alt="..." src="..." />
  <img class="w-8 aspect-square rounded-full ring-4 ring-white" alt="..." src="..." />
</div>

ループ処理を用いずにマークアップする際、同じスタイルを重複して記述しなければならず、後から手をいれる時に手間がかかります。

そこで、div > imgのように、親であるdiv要素から子であるimg要素に対してアクセスしたい動機が生じます。

Tailwind CSSでどう実現するか

Tailwind CSSでは*:バリアントを使用すると、子要素・子孫要素にタグ問わずにアクセスできます。

対象 バリアント
子要素のみ *:
子孫要素 **:

先の例で*:バリアントを使用すると以下のようになります。

<div class="*:w-8 *:aspect-square *:rounded-full *:ring-4 *:ring-white">
  <img alt="..." src="..." />
  <img alt="..." src="..." />
  <img alt="..." src="..." />
  <img alt="..." src="..." />
  <img alt="..." src="..." />
</div>

対象の小孫要素を限定したい場合

対象の小孫要素を限定したい場合はArbitrary variants([条件]の構文)を使用します。

例えば、子のimg要素のみに対して適用したい場合は次のようになります。

<div class="[&>img]:w-8 [&>img]:aspect-square [&>img]:rounded-full [&>img]:ring-4 [&>img]:ring-white">
  <img alt="..." src="https://avatar.iran.liara.run/public" />
  <img alt="..." src="https://avatar.iran.liara.run/public" />
  <img alt="..." src="https://avatar.iran.liara.run/public" />
  <img alt="..." src="https://avatar.iran.liara.run/public" />
  <img alt="..." src="https://avatar.iran.liara.run/public" />
</div>

CSSの子セレクタと同じ要領で条件を記します。

[&>img]:は子セレクタですが、小孫セレクタの場合は[&_img]:になります。

空白は_(アンダースコア)で表現するのがポイントです。

まとめ

今回はTailwind CSSで子孫要素にアクセスする方法をご紹介しました。

同じ要素を繰り返す際に重複するスタイルを記述しなくてよいのが嬉しいですね。

一方で、場面を問わずに親要素にスタイルを詰めこんでしまうと、要素とスタイルの凝集度を下げてしまいTailwind CSSを使うメリットを打ち消しかねないので、使い所には注意が必要です。

「同じ要素がn個以上続く場合」など、ルールを設けて使用することをおすすめします。

Tailwind CSSを使用される方の一助となれば幸いです。

ご精読ありがとうございました。

参考文献

Firebase Cloud Messagingの通知メッセージのタイプについて

こんにちは。エキサイトでアプリエンジニアをしている岡島です。今回はFirebase Cloud Messaging(FCM)の通知について共有していこうと思います。FCMの通知には「通知メッセージ(Notification Message)」と「データメッセージ(Data Message)」の2種類があります。

今回開発において、アプリで独自UIの通知を表示することを検討しました。そのときに通知メッセージとデータメッセージの違いについて理解する必要があったので、調査したことについてまとめます。

通知メッセージについて

通知メッセージはOSの通知システムにより、通知センターに表示されるメッセージです。アプリがバックグラウンドや終了状態の時でも通知を送信することができます。

通知メッセージの形式

{
  "message":{
    "token":"<FCM_TOKEN>",
    "notification":{
      "title":"通知のタイトル",
      "body":"メッセージ"
    }
  }
}

動作

アプリの起動状態により処理が変わります。フォアグラウンドの状態であれば、FCMのonMessage関数で処理を行います。バックグラウンドや終了状態では、OSの通知センターに自動で表示されます。

状態 動作
フォアグラウンド onMessage で受信し、アプリ内で処理を行う
バックグラウンド OSの通知センターに自動で表示される
アプリ終了状態 OSの通知センターに自動で表示される

データメッセージについて

データメッセージは、通知として表示されるのではなく、アプリ側で通知が来たときの挙動を制御することができます。

データメッセージの形式

{
  "message":{
    "token":"<FCM_TOKEN>",
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    }
  }
}

データメッセージではdataというキーに自由にデータを書くことができます。このキーとバリューを用いてアプリ側で制御することができます。

動作

データメッセージでは、通知メッセージタイプと異なり、フォアグラウンドやバックグランド時にアプリ側で通知の処理を制御することができます。

状態 動作
フォアグラウンド onMessage で処理可能
バックグラウンド onBackgroundMessage で処理可能

通知は自動で通知センターには表示されないので、flutter_local_notificationsなどを用いてアプリ側で通知を表示させる必要があります。

通知メッセージとデータメッセージの違い

通知メッセージとデータメッセージの違いは、通知センターに自動で表示されるかどうかです。

  • 通知メッセージ: notification キーを含み、自動で通知が表示される。
  • データメッセージ: data のみを含み、アプリ側で処理が必要。

データメッセージの使用用途例について

データメッセージは、通知が自動的に通知センターに表示されるまでに、ハンドリングすることができます。今回の開発ではアプリ内で独自の通知UIを表示させることを想定していました。他にもどの通知が開かれたかログを取ることにも使えそうです。

実装時の注意点

メッセージのペイロードnotification というキーが含まれている場合は、そのメッセージは通知メッセージとして扱われるので通知センターに自動的に表示されることに注意してください。

まとめ

今回はFCMの「通知メッセージ」と「データメッセージ」についてまとめてみました。通知を実装するときに適切にメッセージのタイプを使い分けることが必要だと思いました。

この記事が誰かのお役に立てれば幸いです。

参考文献

https://firebase.google.com/docs/cloud-messaging/concept-options?hl=ja

Spring BootでBasic認証の設定をする方法

はじめに

こんにちは、新卒2年目の岡崎です。

今回は、Spring BootでBasic認証の設定をする方法を紹介します。

環境

  • Gradleのバージョン
------------------------------------------------------------
Gradle 8.11.1
------------------------------------------------------------

Build time:    2024-11-20 16:56:46 UTC
Revision:      481cb05a490e0ef9f8620f7873b83bd8a72e7c39

Kotlin:        2.0.20
Groovy:        3.0.22
Ant:           Apache Ant(TM) version 1.10.14 compiled on August 16 2023
Launcher JVM:  21.0.5 (Amazon.com Inc. 21.0.5+11-LTS)
OS:            Mac OS X 14.7.2 aarch64
  • Javaのバージョン
openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing)
  • Spring Bootのバージョン
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.0)

準備

SpringSecurityを仕様するため、、build.gradleに以下の依存関係を追加します。

 implementation 'org.springframework.boot:spring-boot-starter-security'

また、コントローラーを以下のように実装しました。

@RestController
public class SampleController {
    @GetMapping("pass")
    public String passBasicAuth() {
        return "pass success!";
    }

    @GetMapping
    public String basicAuth() {
        return "basicAuth success!";
    }
}

実装

まずはBasic認証を行うため、SecurityConfigクラスの実装を作成します。

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                .httpBasic(basic -> {});

        return http.build();
    }
}

ページにアクセスすると、ユーザーとパスワードの入力を求められるようになりました。

デフォルトでは、Spring Bootがユーザー名とパスワードを自動的に生成します。

任意のユーザー名とパスワード名を指定する場合は、application.yamlに以下のように実装します。

spring:
  security:
    user:
      name: sample # ユーザー名
      password: password # パスワード

これで、指定したユーザー名とパスワードで認証できるようになります。

実際に確認してみます。

ユーザー名とパスワードを入力し、ログインボタンを押せば、無事にページの表示ができました。

特定のエンドポイントでBasic認証を不要にしたい場合は、permitAll() を使用します。

http
 .authorizeHttpRequests(auth -> auth
  // Basic認証が不要なエンドポイントを追加する
        .requestMatchers(HttpMethod.GET, "/pass").permitAll()
        .anyRequest().authenticated()
 )
 .httpBasic(basic -> {});

requestMatchersBasic認証を適用しないエンドポイントを指定し、permitAllでアクセスを許可します。

これにより、/passへはBasic認証なしでアクセスできるようになりました。

最後に

今回は、Spring BootでBasic認証の設定をする方法を紹介しました。誰かの参考になれば幸いです。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひ連絡よろしくお願いいたします。

www.wantedly.com

htmxのhx-indicatorとTailwind CSSでスケルトンスクリーンを実現する

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、htmxのhx-indicatorとTailwind CSSを組み合わせてスケルトンスクリーンを実現する方法をご紹介します。

ケルトンスクリーンとは

ケルトンスクリーンは、読み込み中にコンテンツの代替となる図形を表示して、処理が実行中であることなどをユーザーに示すUXテクニックの一つです。

読み込み中はコンテンツの代替となる図形を表示する

Tailwind CSSでスケルトンスクリーンをスタイリングする

Tailwind CSSでは、スケルトンスクリーン用のアニメーションが標準搭載されており、簡単にスタイリングできます。

スタイリングについては、以下の記事で詳しく説明しておりますので本稿では割愛します。

tech.excite.co.jp

htmxでリクエスト中のみ特定の要素を可視化するhx-indicator

htmxでは、リクエスト中のみ特定の要素を可視化する機能として、hx-indicator属性が用意されています。

hx-indicatorの基本

hx-getなどのAJAXリクエストをする属性が付与された要素に、hx-indicator属性を追加して値にはリクエスト中に可視化したい要素のidかclassを指定します。

また、可視化したい要素のclassにhtmx-indicatorを指定します。

<div>
  <button type="button" hx-get="..." hx-indicator="#loading">Request</button>
  
  <!-- リクエスト中は以下の要素が表示される -->
  <div id="loading" class="htmx-indicator">...</div>
</div>

仕組みは単純です。

hx-indicatorを使用すると、htmxによって自動的に以下のスタイルがhtmlに挿入されます。

<style>
  .htmx-indicator{
    opacity:0;
    transition: opacity 500ms ease-in;
  }
  .htmx-request .htmx-indicator{
    opacity:1;
  }
  .htmx-request.htmx-indicator{
    opacity:1;
  }
</style>

処理の流れは次の通りです。

  1. htmx-indicatoropacity:0;(不透明度0%)が指定されている
  2. リクエスト中にはclassにhtmx-requestが加えられる
  3. htmx-requestと同居する場合はhtmx-indicatoropacity:1;(不透明度100%)になり要素が可視化される

Tailwind CSSと組み合わせる

リクエスト中にTailwind CSSでスタイリングしたスケルトンスクリーンを表示させたいとします。

See the Pen Untitled by AyumuSaito (@ayumusaito-excite) on CodePen.

<button type="button" hx-get="..." hx-indicator="#loading">Request</button>

<!-- リクエスト中は以下の要素を表示させたい -->
<div id="loading" class="htmx-indicator">
  <div class="p-4 rounded bg-white border border-slate-300">
    <div class="animate-pulse flex gap-3">
      <div class="w-12 h-12 aspect-square rounded-full bg-slate-300"></div>

      <div class="flex flex-col gap-2">
        <div class="h-6 w-24 rounded bg-slate-300"></div>
        <div class="h-6 w-40 rounded bg-slate-300"></div>
        <div class="h-6 w-80 rounded bg-slate-300"></div>
      </div>
    </div>
  </div>
</div>

注意点

この時に注意が必要です。

というのも、標準のhx-indicatorのスタイルはopacityを使用して要素の可視化を行うため、スケルトンスクリーン用の要素のスペースが常に確保されてしまいます。

確保されたスケルトンスクリーン用の要素のスペース

そのため、hx-indicator用のスタイルを独自定義する必要があります。

スペースの確保を回避するためには、opacityではなくdisplayを用いて制御するようにします。

独自定義するスタイルのclass名はhtmx-indicator以外を使用します。

<style>
  .my-indicator{
    display:none;
  }
  .htmx-request .my-indicator{
    display:block;
  }
  .htmx-request.my-indicator{
    display:block;
  }
</style>

完成形

先に述べた独自定義のスタイルを使用した完成形は以下の通りです。

<head>
  ...

  <!-- displayを使用するように独自定義したhx-indicator用のスタイル -->
  <style>
    .my-indicator{
      display:none;
    }
    .htmx-request .my-indicator{
      display:block;
    }
    .htmx-request.my-indicator{
      display:block;
    }
  </style>
</head>

<body>
  <button type="button" hx-get="..." hx-indicator="#loading">Request</button>
  
  <!-- リクエスト中は以下の要素が表示される -->
  <div id="loading" class="my-indicator">
    <div class="p-4 rounded bg-white border border-slate-300">
      <div class="animate-pulse flex gap-3">
        <div class="w-12 h-12 aspect-square rounded-full bg-slate-300"></div>
  
        <div class="flex flex-col gap-2">
          <div class="h-6 w-24 rounded bg-slate-300"></div>
          <div class="h-6 w-40 rounded bg-slate-300"></div>
          <div class="h-6 w-80 rounded bg-slate-300"></div>
        </div>
      </div>
    </div>
  </div>
</body>

hx-indicatorとTailwind CSSを組み合わせたスケルトンスクリーン

さいごに

今回は、htmxのhx-indicatorとTailwind CSSを組み合わせてスケルトンスクリーンを実現する方法をご紹介しました。

実装が手間なスケルトンスクリーンですが、htmxを使用することにより、JSを書かずにマークアップと同じ感覚で実現できます。

htmx、Tailwind CSSを使用される方の一助となれば幸いです。

ご精読ありがとうございました。

Tailwind CSSでコンテナークエリを使用する方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

2025年1月23日にTailwind CSSのv4の安定版がリリースされました。新機能の一つにコンテナークエリーのサポートがあります。

今回はTailwind CSSでコンテナークエリを使用する方法をご紹介します。

コンテナークエリーとは?

コンテナークエリーとは、特定の親要素(コンテナー)のサイズに基づいたスタイリングを可能にする機能です。

メディアクエリーは画面サイズですが、コンテナークエリは特定の要素のサイズを基準とするため、より柔軟にレスポンシブ対応が可能です。

以下の記事で詳しく説明していますので、合わせてご覧ください。

tech.excite.co.jp

簡単におさらい

純粋なCSSでコンテナークエリーを使用する場合、実装のポイントは以下の通りです。

  • 基準とする要素にcontainer-typeを付与する
  • @container(...)とメディアクエリーと同じような形式でスタイリングする
  • container-nameを使用すると基準とする要素の名前を明示できる

完成形は以下の通りです。

<div class="parent">
    <div class="child">...</div>
</div>

<style>
    .parent {
        container-type: inline-size;
        container-name: parent;
    }
    
    .child {
        background-color: black;

        @container parent (max-width: 512px) {
            /* .parentの横幅が512px以下のときに背景色が赤になる */
            background-color: red;
        }
    }
</style>

Tailwind CSSでスタイリングする

基準とする要素に@containerを追加

基準とする要素に@containerを追加します。@containerにはcontainer-type: inline-size;が含まれます。

<div class="@container"></div>

@container/{name}の形式でcontainer-nameが使用できます。

<div class="@container/parent"></div>

@{size}:*の形式でスタイリング

基準を使用する要素では@{size}:*の形式でスタイリングします。

コンテナーサイズトークン一覧は以下で確認できます。

tailwindcss.com

例えば、「基準とする要素の横幅が512px以上の場合に背景色が赤になる」という条件の場合は以下のようになります。

<div class="@container">
    <div class="@lg:bg-red-500"></div>
</div>

max-widthを表現したい場合は@max-{size}:*の形式にします。 「基準とする要素の横幅が512px以下の場合に背景色が赤になる」という条件の場合は以下のようになります。

<div class="@container">
    <div class="@max-lg:bg-red-500"></div>
</div>

container-nameを使用している場合は@{size}/{name}:*の形式を使用します。

<div class="@container/parent">
    <div class="@lg/parent:bg-red-500"></div>
</div>

完成形

純粋なCSSと比較した完成形です。

純粋なCSS

<div class="parent">
    <div class="child">...</div>
</div>

<style>
    .parent {
        container-type: inline-size;
        container-name: parent;
        ...
    }
    
    .child {
        ...
        @container parent (max-width: 512px) {
            background-color: var(--red-500);
        }
    }
</style>

Tailwind CSS

<div class="@container/parent">
    <div class="@lg/parent:bg-red-500">...</div>
</div>

さいごに

今回はTailwind CSSでコンテナークエリを使用する方法をご紹介しました。

v4のコンテナークエリーのサポートで、表現の幅がより広まったと思います。

Tailwind CSSはHTMLに対してスタイルを直書きするため、HTMLから離れずに追うことができ、コンテナークエリーなどの複雑なスタイリングの理解もしやすいのではないかと思います。

Tailwind CSSを使用される方の一助となれば幸いです。

ご精読ありがとうございました。

参考文献

tailwindcss.com

Tailwind CSSのv3からv4でテーマ設定の方法がどう変わるか

こんにちは。エキサイトでデザイナーをしている齋藤です。

Tailwind CSSのメンテナーの一人のAdam Wathan氏は、日本時間の1月16日に自身のXにおいて、「Tailwind CSS v4.0安定版は来週リリース予定」と発表しました。

予定通りであれば、今週中にTailwind CSS v4が正式にリリースされるようです。

大きな変更点として、tailwind.config.jsが廃止され、CSSファイルで設定を行うようになることがあります。

tailwindcss.com

今回は、v3からv4でテーマ設定の方法がどう変わるかについて、代表的なスタイル要素を例にご紹介します。

免責

この記事でご紹介する内容は、2025年1月20日時点のTailwind CSS v4.0 Betaに準じており、安定版での動作を保証するものではありません。

Tailwind CSS v4ベータ版の準備

今回はベータ版にて検証をします。

まずは、Tailwind CSSCLIのベータ版をインストールします。

npm install tailwindcss@next @tailwindcss/cli@next

次に、設定を書くCSSファイル(app.css)を作成し、Tailwind CSSをインポートします。

@import "tailwindcss";

最後に以下を実行すると、設定を書くCSSファイル(app.css)をもとに、最終的なCSSファイル(gen-output.css)が生成されます。

npx @tailwindcss/cli -i app.css -o gen-output.css

※ローカル環境でwatchしたい場合は、末尾に--watchを付けると差分を検知してリアルタイムにCSSを生成できます。

テーマ設定とは?

Tailwind CSSはカラーや角丸の値などの基本のデザイン要素が標準で定義されています。

これらのカスタマイズをテーマ設定と呼びます。

v3ではtailwind.config.jsにて設定しましたが、v4からはCSSファイルになります。

v3からv4でのテーマ設定方法

今回は以下のスタイル要素の設定方法がどう変わるかをご紹介します。

カラー

v3での設定方法

v3ではtheme.colorに定義をしました。

/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    color: {
      'black': '#020617',
    }
  }
}

v4での設定方法

v4ではプレフィックスcolorを付けて、CSS変数形式で定義します。

@import "tailwindcss";

@theme {
  --color-black: #020617;
}

結果

<!-- colorが#020617になる -->
<p class="text-black">...</p>

タイポグラフィ

v3での設定方法

v3ではtheme.fontSizeに定義をしました。

/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    fontSize: {
      'base': ["1rem", { fontWeight: "400", lineHeight: "1.8", letterSpacing: "0.02em" }],
    }
  }
}

v4での設定方法

v4ではプレフィックスtextを付けて、CSS変数形式で定義します。

ユーティリティクラスのtext-*にフォントサイズに加えて、font-weightline-heightletter-spacingの値も含めたい場合は、--text-*--font-weightの形式で続けると関連付けされます。

@import "tailwindcss";

@theme {
  --text-base: 1rem;
  --text-base--font-weight: 400;
  --text-base--line-height: 1.8;
  --text-base--letter-spacing: 0.02em;
}

結果

<!-- font-sizeが1rem、font-weightが400、line-heightが1.8、letter-spacingが0.02emになる -->
<p class="text-base">...</p>

角丸

v3での設定方法

v3ではtheme.borderRadiusに定義をしました。

/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    borderRadius: {
      lg: '1rem'
    }
  }
}

v4での設定方法

v4ではプレフィックスradiusを付けて、CSS変数形式で定義します。

@import "tailwindcss";

@theme {
  --radius-lg: 1rem;
}

結果

<!-- border-radiusが1remになる -->
<div class="rounded-lg">...</div>

ボックスシャドウ

v3での設定方法

v3ではtheme.boxShadowに定義をしました。

/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    boxShadow: {
      'sm': '0 1px 2px 0 rgb(0 0 0 / 0.03)',
    }
  }
}

v4での設定方法

v4ではプレフィックスshadowを付けて、CSS変数形式で定義します。

@import "tailwindcss";

@theme {
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.03);
}

結果

<!-- box-shadowが0 1px 2px 0 rgb(0 0 0 / 0.03)になる -->
<p class="shadow-sm">...</p>

ブレイクポイント

v3での設定方法

v3ではtheme.screensに定義をしました。

/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    screens: {
      'sm': '480px',
    }
  }
}

v4での設定方法

v4ではプレフィックスbreakpointを付けて、CSS変数形式で定義します。

@import "tailwindcss";

@theme {
  --breakpoint-sm: 480px;
}

結果

<!-- 画面が480px以上のときにcolorがred-500になる -->
<p class="sm:text-red-500">...</p>

注意: v4からはmax-width指定はmax-*でclassとして指定が推奨されています

v3では 'sm': { max: '480px' },のように、ブレイクポイントをmax-widthで指定できました。

v4のCSSファイルでの設定でのmax-widthの指定をせずに、スタイリング時にmax-*の形式で指定することが推奨されます*1

<!-- (width < {smの値})になる -->
<p class="max-sm:text-red-500"></p>

まとめ

今回は、v3からv4でテーマ設定の方法がどう変わるかについて、代表的なスタイル要素を例にご紹介しました。

v4からはJSを一切書かずにCSSファイルだけでテーマ設定が可能になることで、導入や運用のハードルが下がるかと思われます。

v4の詳しい変更内容は、公式ドキュメントをご覧ください。

tailwindcss.com

Tailwind CSSを使用される方の一助となれば幸いです。

ご精読ありがとうございました。

参考文献

Pangleメディエーションのエラーとその回避策

こんにちは。エキサイトでアプリエンジニアをしている岡島です。

今回はPangleのメディエーション時に起きたエラーと今回行った対処について共有していこうと思います。

環境

Flutter: 3.24.2
Dart: 3.5.2
CocoaPods: 1.15.2
gma_mediation_pangle: 1.1.0

発生した問題

Pangleのメディエーションを行い、pod installをすると下記エラーが出ました。

[!] The 'Pods-Runner' target has transitive dependencies that include statically linked binaries: 

このエラーは、CocoaPodsがターゲットに含まれる静的ライブラリの依存関係を正しく解決できない場合に発生するようです。

対応方法

Podfileに以下のコードを追記しました。

pre_install do |installer|
  Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
end

このコードは、CocoaPodsの静的ライブラリに関する検証をスキップする一時的な回避策です。根本的な問題解決ではありませんが、ほかに対応できる術が見つからずこのように対応しました。

まとめ

今回のエラー対応では、Podfileにコードを追加することで一時的に問題を回避しました。しかし、この方法は根本的な解決策ではないため、ライブラリをアップデートするなど解決策が見つかれば早急に対応したいと思います。

この記事が、同じ問題に直面した方々の参考になれば幸いです。

参考資料

【Flutter】flutter_image_compressが対応する画像入出力形式について

こんにちは。エキサイトでアプリエンジニアをしている岡島です。

今回は、画像圧縮を行うライブラリであるflutter_image_compressの対応画像形式について調べたのでまとめていこうと思います。

環境

flutter_image_compress: 2.4.0

画像圧縮方法

flutter_image_compressを用いると下記コードのように、圧縮したい画像ファイルのpathと出力先のpathを指定し、画像品質をqualityで指定すると画像圧縮ができます。

final result = await FlutterImageCompress.compressAndGetFile(
    filePath, // 画像ファイルのパス
    targetPath, // 出力先のパス
    quality: 88, // 画像品質
  );

ファイルを圧縮してUint8Listを取得する関数やUint8List形式のデータを圧縮する関数も用意されているので、詳しくはライブラリのReadmeをご参照ください。

サポートされる画像形式

この記事ではiOSAndroidのプラットフォームについて、サポートされている画像形式を見ていきます。

入力形式

出力形式

出力形式 iOS対応 Android対応
jpeg
png
heic
webp

heic形式はiOS 11以上、Android API 28以上でサポートされています。

出力形式の指定方法

enumCompressFormatが用意されているので、CompressFormat.jpegのようにformatを指定します。 デフォルトではjpeg形式に設定されています。

final result = await FlutterImageCompress.compressAndGetFile(
    filePath,
    targetPath, 
    quality: 88,
    format: CompressFormat.png,  // 出力formatを指定
  );

最後に

今回はflutter_image_compressで対応しているファイル形式についてまとめてみました。 この記事が誰かのお役に立てれば幸いです。

コンテナークエリーを使用して親要素のサイズに応じたスタイリングをする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、コンテナークエリーを使用して親要素のサイズに応じたスタイリングをする方法をご紹介します。

コンテナークエリーとは?

コンテナークエリーとは、特定の親要素(コンテナー)のサイズに基づいたスタイリングを可能にする機能です。

メディアクエリーは画面サイズですが、コンテナークエリは特定の要素のサイズを基準とするため、より柔軟にレスポンシブ対応が可能です。

使い方

コンテナークエリを用いたスタイリングの例をお示しします。

HTML

HTMLの構造は以下の通りです。

<div class="parent">
  <div class="child"></div>
</div>

.parentの横幅に基づいて.childをスタイリングします。

CSS

基本のスタイルは以下の通りです。

.childは背景色が黒の128pxの正方形になるようにしています。

.parent {}

.child {
    aspect-ratio: 1/1;
    background: black;
    width: 128px;
}

コンテナークエリを有効にする

コンテナークエリを有効にするために、基準となる要素にcontainer-typeを付与する必要があります。

container-typeには3種類の値が存在します。

挙動
size インライン(横幅)およびブロック(縦幅)の両方の寸法が基準に使用されます
inline-size インライン(横幅)の寸法が基準に使用されます
normal 実質的にコンテナークエリーが無効になります

今回の例では、.parentの横幅に基づいて.childをスタイリングするため、inline-sizeを使用します。

.parentcontainer-type: inline-size;を追加します。

.parent {
    container-type: inline-size;
}

コンテナークエリーを使用してスタイリングする

今回は、「.parentの横幅が512px以下のとき、.childの背景色が赤になる」を条件とします。

コンテナークエリー@container記法(メディアクエリーの@mediaと同じ)でスタイリングすると、以下のようになります。

.child {
    ...

    @container (max-width: 512px) {
        background: red;
    }
}

結果

.parentの横幅が512px以下のとき、.childの背景色が赤に変化するようになりました。

.parentの横幅に応じて.childの背景色が変わる

基準としている親要素を明示する

先にお示しした例のままですと、基準としている親要素が分かりづらく、コンテナークエリーを有効にした要素が複数ある場合に混乱の要因になってしまいます。

.child {
 ...

    /* どの要素の横幅なのかが分からない */
    @container (max-width: 512px) {
        ...
    }
}

そこで、親要素にcontaner-nameを付与し、@container内で基準とする親要素を明示します。

以下のようにcontaner-nameを使用します。(その際、値は引用符で囲わないように注意してください)

.parent {
    ...

    container-name: parentElement;
}

.child {
    ...

    @container parentElement (max-width: 512px) {
        ...
    }
}

なお、container-nameを使用しない場合は、一番近いコンテナークエリーを有効にした要素が基準となります。

まとめ

今回は、コンテナークエリーを使用して親要素のサイズに応じたスタイリングをする方法をご紹介しました。

従来のメディアクエリーでは画面幅基準のみのレスポンシブ対応でしたが、コンテナークエリーを使用することでより柔軟にできるようになりました。

スタイリングされる方の一助となれば幸いです。

ご精読ありがとうございました。

【AI活用】VRoid Studio・copainter・CLIP STUDIOを使ってコミック制作してみた

はじめに

こんにちは、エキサイト株式会社デザイナーの山﨑です。

エキサイトホールディングス Advent Calendar 2024 18日目は、山﨑が担当させていただきます。

qiita.com

今回は、VRoid Studio・copainter・CLIP STUDIO PAINTを用いてのコミック制作ついて、記して行きたいと思います。

AIを用いたコミック制作にチャレンジするまでの経緯

エキサイトでは、ウーマンエキサイトをはじめ、多くのコミックコンテンツを発信しています。

しかし、コミック制作には手作業が多く、時間やコストがかかるという課題がありました。

そこで、「AIツールを活用すれば、よりコミック制作の効率化が図れるのではないか?」という発想から、この挑戦がスタートしました。

今回の検証ポイントはこのようになります:

  1. 手作業の削減はどの程度可能か?
  2. 制作時間はどのくらい短縮できるか?
  3. 商業レベルのクオリティに達するか?

自分で絵を描かずにVRoid Studio、copainter、CLIP STUDIO PAINTを組み合わせて、どれほどスムーズにコミックが作れるか実際にやってみました!

ツールの説明

コミック制作に使用した主なツールを簡単に紹介します:

VRoid Studio:3Dキャラクターを簡単に作成できるソフトウェア。

vroid.com

copainter:ラフから線画を起こし、下塗り・着彩を行うAIイラスト制作支援ツール。

www.copainter.ai

CLIP STUDIO PAINT:プロ仕様のイラスト、漫画制作ソフトウェア。

www.clipstudio.net

今回の流れ:

  1. CLIP STUDIO PAINTでセリフ入れ
  2. VRoid Studioでモデル制作
  3. copainterでモデルをコミック風イラストに変換
  4. CLIP STUDIO PAINTで背景や効果などの最終調整

セリフ入れ

まずは、CLIP STUDIO PAINTでコマ割りを作成し、セリフを入れていきます。(プロット〜ネーム部分は割愛させてください。)

モデル制作

登場人物は以下の3人です:

主人公(女性・黒髪ショート) 主人公の友達(女性・茶髪ロング) 主人公の彼氏(男性・茶髪ショート)

髪型・服装を見分けやすいデザインにしました。(似たような顔になることが多いので、髪色・髪型・洋服で差別化します。)

VRoid Studioでは、顔、髪型、衣装のセットが用意されており、簡単にモデルを作成できます。また、細かいカスタマイズも可能です。

さらに、VRoid用素材はBOOTH(pixivが運営するマーケットプレイス)からダウンロードして利用できます。

モデルにポーズを取らせる

VRoid Studioの強みは、モデルに表情やポーズを取らせた状態で写真を撮影できることです。 テンプレートモーションを使えば、自然な動きの瞬間をキャプチャすることができます。

このように、漫画のコマに沿ったポーズをモデルに取らせて写真を撮影していきます。

コミックイラスト化

撮影した画像をcopainterでコミックイラストに変換します。

copainterの「ペン入れ」から撮影した画像を入れ…

忠実度やその他の数値を調整すると…

コミックイラスト化できました。

このようにどんどんコミックイラスト変換して行きます。

泣き顔、頬を赤らめさせた表情にしたい場合は、画像の上に涙や頬に車線を加えるだけで、AIが自動で処理してくれます。

また、小物や背景は以下の方法で準備しました:

小物(財布など):CLIP STUDIO ASSETSから3Dモデルをダウンロード→スクショを撮影→copainterで出力。

背景:Adobe Stockから写真をダウンロード→copainterでコミックイラスト化。

イラストを合成

最終的に、作成したイラストをCLIP STUDIO PAINTでコマに組み込みます。 効果や文字入れを追加し、完成したのが以下の作品です:

検証結果

①手作業の削減はどの程度可能か? → 手作業の大部分である絵を描く必要がなくなるので大幅に削減できた。

ただし、セリフ入れや演出、モデルにポーズを取らせるスキルが必要になるため、完全な自動化は難しい。

②制作時間はどのくらい短縮できるか? → 約2~3時間で制作完了。従来なら8~10時間以上かかる作業を大幅に短縮できた。

③商業レベルのクオリティに達するか? → 出来上がりはかなり自然で、3Dモデルだと気付かれないほどのクオリティに仕上がった。

最後に

今回はモデルのポーズの取らせ方などは割愛してしまいましたが、今回の制作で、VRoid Studioやcopainterの可能性を実感しました!

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひ連絡よろしくお願いいたします!🙇

www.wantedly.com

Spring Bootにおいて、Controllerで全てのリクエストを受け取る方法

はじめに

こんにちは、新卒2年目の岡崎です。

エキサイトホールディングス Advent Calendar 2024の20日目を担当します。

他の記事はこちらから!

qiita.com

今回はSpring Bootにおいて、Controllerで全てのリクエストを受け取る方法を紹介します。

環境

  • gradle
------------------------------------------------------------
Gradle 8.11.1
------------------------------------------------------------

Build time:    2024-11-20 16:56:46 UTC
Revision:      481cb05a490e0ef9f8620f7873b83bd8a72e7c39

Kotlin:        2.0.20
Groovy:        3.0.22
Ant:           Apache Ant(TM) version 1.10.14 compiled on August 16 2023
Launcher JVM:  21.0.5 (Amazon.com Inc. 21.0.5+11-LTS)
Daemon JVM:    /Users/hiromi.okazaki/.sdkman/candidates/java/21.0.5-amzn (no JDK specified, using current Java home)
OS:            Mac OS X 14.7.1 aarch64
openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing)
  • Spring Boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.0)

実装

Spring Bootで全てのリクエストをControllerで受けとる場合、パスを/**とすれば実現できます。

    @RequestMapping("/**")
    public String getAllRequest() {
        return "hello world";
    }

@RequestMappingを使用すると、GETとPOSTの区別をつけず、リクエストを受け取ることができます。

また、GETのみ全てのリクエストを受け取りたい場合は、@GetMapping("/**")、POSTのみ全てのリクエストを受け取りたい場合は@PostMapping("/**")とすればそれぞれのリクエストを受け取ることができます。

特定のエンドポイントが存在する場合

特定のエンドポイントが存在する場合は、/**ワイルドカード指定)よりも優先されます。

例えば、/sampleというエンドポイントが定義されている場合、リクエストは/**ではなく /sampleにルーティングされます。

最後に

本日はSpring Bootにおいて、Controllerで全てのリクエストを受け取る方法を紹介しました。どなたかの助けになれば幸いです。

小さなデザイナーチームで実践!レビュー依頼のワークフローを設計した話

こんにちは!SaaS・DX事業部デザイナーの鍜治本です。
前回、ブランチ機能を駆使してUIファイル管理している記事を書きました。

tech.excite.co.jp

事業部デザインチームでのUI更新方法が型化され、日々の作業が少し楽になった一方で、新たな課題が見えてきました。
それはレビュー依頼のやりとりです。この記事では、スラックのワークフロー機能を活用し、レビュー依頼を効率化した方法をご紹介します。

背景:レビュー依頼の課題

チームでUI更新の作業が分担できるようになった中、作業内容をレビューしてもらうにあたり課題が浮かび上がりました。

メンバーそれぞれのレビュー依頼

アナウンス手段が統一されていない

各メンバーがUI作成後にSlackで都度レビュー依頼を出しており、チャンネルは決まっているものの明確なルールは定義していませんでした。
「レビューの依頼はこのチャンネルでいいんでしたっけ…?」とメンバーに聞き、口頭で確認をとっている状態に…

依頼内容を作成するの負担が大きい

「修正なのか新規なのか」「どの箇所を見てほしいのか」など、依頼内容を毎回手書きで詳細に書くのが手間に感じる場面が多かったです。
また、プロトタイプを作成するとそれについても説明が必要に。レビュー依頼するたびに「依頼内容を説明」する必要があり、地味な負債となっていました。

これらの課題は特に、経験が浅いメンバーにとって心理的ハードルが高く、依頼が滞る原因にもなってしまいます。

解決策:ワークフロー機能を活用

この課題を解消するために、Slackのワークフロー機能を活用することに。
ワークフロー自体はデザイナー組織でも活用事例があるので、よければこちらもご覧ください。 tech.excite.co.jp

ワークフローを使えば、自己流になりがちなレビュー依頼も型化できます。
仕組み自体は簡単ですが、レビュー依頼は次のような流れにしました。

ワークフローの流れ

ワークフローの概要図
①ワークフローを立ち上げる
レビューを依頼したいメンバーがワークフローを起動。

②レビュー内容の入力
モーダルの内容に沿って、「レビューしてほしいリンク」「確認箇所」「その他備考」をフォームに記述して送信。

③該当チャンネルで通知
フォーム内容がレビュー担当者に通知され、次のアクションを促します。通知内容にはレビュー内容で記述されたリンクや、確認箇所が記載されています。

④レビュー担当者によるレビュー
レビュー担当者がボタンを押して、②レビュー内容の入力と同じくモーダルが立ち上げる。モーダルにはフォーム形式で「レビュー内容」「打合せ緊急度」を入力できるので、レビュー内容に合わせて記述&送信。

⑤レビュー依頼者への通知と共有
フォームが送信されると、レビュー依頼時のスレッドに④の内容が投稿される。

レビュー依頼者とレビュー担当者それぞれのフォーム

複数人がレビュー担当者としてレビューをする場合も、④のステップから開始でき、全て同じスレッドにまとめられます。

実際に使ってみて感じたこと

仕組み自体はシンプルですが、フォーム形式で入力できる点が大きなポイント。毎回のテキスト作成にかかる負担が軽減され、実際に自分以外のメンバーにも導入してもらっています。

ワークフローなので内容が統一され、担当者が確認しやすくなりました
元々、メンバーからも「レビューを投げる時に何を書けばいいか…?」という疑問があがっていたので、改めてレビューの流れを整頓できました。
特に、次の様なメリットがあったと感じています。

心理的ハードルを軽減できる

レビュー依頼やフィードバックのやりとり自体が簡単になり、依頼を投げるための準備といった仕込み作業を減らせました。これによって、本来のレビューに集中できるようになりますね。

必要な情報を押さえられる

依頼時に重要な要素となる「レビューで伝えるべき前情報」も必ず入力できます。新卒メンバーなど、レビュー依頼に慣れていない人もスムーズに依頼できるようになりました。

今後の展望

現在はSlack上で通知・内容確認し、具体的な修正などはミーティングをセットする流れとなっています。
ただ、これではレビュー内容と実タスクとの紐付きが弱く、後から見返した時の関連が分かりにい状態です。また、別タスクでも活かせる情報源でもあることから、レビューした内容自体にも価値がある考えています。

今後は、タスク管理をしているNotionと連携させて、もらったレビュー内容やフィードバックを関連づけたり、レビュー内容をチームのナレッジベースとして活用できないか調整してみます。

まとめ

今回の取り組みを通じて、チーム全体のレビュー依頼プロセスが出来上がったことで、メンバー同士でレビューする際の負担を減らせました。
小さな工夫でしたが、チームのコミュニケーションを円滑にできる環境作りになれたと思います。
皆さんも、ぜひワークフロー機能を試してみてください〜!